diff --git a/.cursor/rules/living-documentation.mdc b/.cursor/rules/living-documentation.mdc new file mode 100644 index 0000000..2387b69 --- /dev/null +++ b/.cursor/rules/living-documentation.mdc @@ -0,0 +1,50 @@ +--- +description: Document new types and members — purpose, usage, problem solved +alwaysApply: true +--- + +# Living Documentation for Types and Members + +Good code documents **why** it exists, not only **what** it does. When you add or materially change a type (class, struct, enum, type alias) or a non-obvious member, add brief comments at the definition site. + +## Required for new/changed definitions + +For each new type or group of related helpers, document: + +1. **Purpose** — what problem this solves in the subsystem. +2. **Usage** — who calls it, lifetime/ownership expectations, borrow vs own. +3. **Scope** — file-local helper vs public API; thread/context constraints if relevant. + +For non-obvious members (handles, caches, flags, borrowed pointers, `unique_ptr` sinks), add a comment **on the line above the member** unless the type-level comment already makes it obvious. Do not use trailing end-of-line comments for member documentation. + +## Style in this repo + +- **Public headers** (`cpp-lib/include/`, generated interfaces): use existing `/*! ... */` block style where siblings do. +- **Implementation-only helpers** (anonymous namespaces, `.cpp` RAII guards): `//` block above the type; `//` line above each non-obvious member. +- Match **line density** of the surrounding file — do not add blank lines after every line. +- Do not restate the identifier in prose (“`m_foo` is the foo”) unless it disambiguates ownership or invariants. +- **Document each type or member in place.** Do not defer to another definition with “same as”, “like”, “see”, or “mirrors” — if one changes, cross-references go stale. Repeat purpose, usage, and ownership at every site that needs it. + +## Examples + +```cpp +// Snapshot of inbound invoke metadata after envelope decode. +class InboundInvokeRejectContext { +public: + // Invoke ID from the decoded envelope (99999 = event). + AsnIntType m_invokeId = 0; +}; + +/*! Owned inbound response matched to a pending outbound invoke. */ +std::unique_ptr m_pAnswerMessage; +``` + +## When comments are not required + +- Trivial locals, loop indices, obvious parameters (`invokeID`, `strResponse`). +- Generated code unless the generator template is being changed. +- Renames with unchanged semantics. + +## Agent workflow + +When introducing structures or members as part of a fix or feature, **include documentation in the same change**. Do not defer “comment pass” to a follow-up unless the user explicitly asks for code-only first. diff --git a/.cursor/rules/test-specification-first.mdc b/.cursor/rules/test-specification-first.mdc new file mode 100644 index 0000000..acd318b --- /dev/null +++ b/.cursor/rules/test-specification-first.mdc @@ -0,0 +1,53 @@ +--- +description: Tests specify public API behavior — never tune tests or product code to go green +alwaysApply: true +--- + +# Tests Define the Public API + +**Hard constraint for the snacc ecosystem (compiler, runtimes, generated stubs, samples).** + +Tests document the **expected behavior** of the public API. They are the specification callers and integrators may rely on — not a mirror of whatever the code happens to do today. + +## Non-negotiable rules + +1. **Write tests to specify the API.** The goal is a precise, reviewable contract for public behavior (encoders, decoders, ROSE runtime, generated handlers, etc.) — not merely a green test count. + +2. **NEVER tune test code** to make the suite pass. Do not weaken, delete, skip, or rewrite assertions because the current implementation fails. Do not change expected return codes, context identity, telemetry shape, or error semantics to match existing behavior. + +3. **NEVER tune production or library code** (compiler, `cpp-lib/`, `c-lib/`, generators, runtimes) merely to make tests pass. Do not add shortcuts, bypasses, test-only branches, or special cases whose purpose is to turn a failing spec test green without implementing the specified behavior. + +4. **NEVER tune both sides** in the same change to "meet in the middle." + +5. **When writing tests, do not change test or product code** beyond adding the new test files and neutral test infrastructure (fixtures, CMake wiring) that does not alter what is asserted. + +6. **Only exception:** the user **explicitly asks** to tune test code, tune product code, or revise the spec. Until then, leave failing tests failing and report what failed and why. + +7. **Test-first workflow:** when asked to add tests before fixes, write the **full** scenario matrix (happy + failure paths), run it, and **stop**. Do not fix product code in the same task unless the user asks for that phase. + +8. **Harness and fakes are for observation only.** Test support (`cpp-lib/tests/test_support/`, mocks, loopback transports) may record state and drive scenarios. It must **not** bypass the API under test or smuggle in behavior the real runtime would not satisfy. + +9. **If the spec is unclear, ask.** Do not guess by aligning test and code. Document locked-in semantics in `cpp-lib/tests/runtime_correctness_notes.md`. + +## When a spec test fails (default behavior) + +- **Report** the failure and which spec assertion is unmet. +- **Do not** change the test or product code unless the user explicitly requests it. +- Neutral test infrastructure (fixtures, capture helpers) is allowed if it does not change what is asserted. + +## Forbidden unless the user explicitly asks + +- Weakening or removing assertions +- Changing expected values to match current output +- Harness workarounds that fake green results +- Production hacks (`if (testing)`, alternate code paths for tests) +- `DISABLED` / skipped tests without user approval + +## Primary paths + +- C++ runtime tests: `cpp-lib/tests/` +- Semantics notes: `cpp-lib/tests/runtime_correctness_notes.md` +- ROSE runtime: `cpp-lib/src/SnaccROSEBase.cpp` +- TypeScript samples: `samples/ts-microservice/` + +**The test specifies what the public API must do. Neither tests nor product code are tuned to go green unless the user specially asks.** diff --git a/AGENTS.md b/AGENTS.md index 15f09cd..af7b54e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,6 +97,18 @@ Requirements and IDE-specific steps are in [ReadMe.md](ReadMe.md). CMake minimum - Prefer the smallest correct change; match surrounding code style. - Generated output shape is defined by ASN.1 inputs plus the relevant `compiler/back-ends/*-gen` implementation — read both before changing behavior. - When documentation and code disagree, verify against `samples/` and tests before updating docs. +- **Document new types and members** at the definition site (purpose, usage, ownership). See [.cursor/rules/living-documentation.mdc](.cursor/rules/living-documentation.mdc). + +### Tests define the public API (hard constraint) + +See [.cursor/rules/test-specification-first.mdc](.cursor/rules/test-specification-first.mdc). + +- Tests specify the **expected behavior** of the snacc ecosystem public API — they are the spec, not a mirror of current code. +- **NEVER tune test code** to match broken or incomplete implementation. +- **NEVER tune product or library code** merely to turn a failing spec test green. +- **NEVER tune both** in one change. +- When writing tests, focus on specifying the API — not on getting a green suite. +- **Only exception:** the user explicitly asks to change tests, change product code, or revise the spec. ## Related repositories diff --git a/compiler/back-ends/c++-gen/gen-code.c b/compiler/back-ends/c++-gen/gen-code.c index 52a7e50..e43001e 100644 --- a/compiler/back-ends/c++-gen/gen-code.c +++ b/compiler/back-ends/c++-gen/gen-code.c @@ -1178,10 +1178,10 @@ static void PrintROSEOnInvokeHandler(FILE* src, int bEvents, Module* mod, ValueD const char* pszHandlerPrefix = bEvents ? "RoseEvent" : "RoseInvoke"; fprintf(src, "// [%s] OPID_%s\n", __FUNCTION__, vd->definedName); - fprintf(src, "static long %s_OPID_%s(SNACC::ROSEMessage* pMsg, SNACC::ROSEInvoke* pInvoke, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", pszHandlerPrefix, vd->definedName, mod->ROSEClassName); + fprintf(src, "static long %s_OPID_%s(std::unique_ptr& pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", pszHandlerPrefix, vd->definedName, mod->ROSEClassName); fprintf(src, "{\n"); fprintf(src, "\t%s argument;\n", pszArgument); - fprintf(src, "\tlong lRoseResult = pBase->DecodeInvoke(pMsg, &argument);\n"); + fprintf(src, "\tlong lRoseResult = pBase->DecodeInvoke(*pMsg, &argument);\n"); fprintf(src, "\tif (lRoseResult == ROSE_NOERROR)\n"); const bool bIsDeprecated = IsDeprecatedFlaggedOperation(mod, vd->definedName); const bool bIsMultiLine = bIsDeprecated || pszResult; @@ -1202,12 +1202,12 @@ static void PrintROSEOnInvokeHandler(FILE* src, int bEvents, Module* mod, ValueD { fprintf(src, "\t\t%s error;\n", pszError); fprintf(src, "\t\tauto invokeResult = pInt->OnInvoke_%s(&argument, &result, &error, ctx);\n", vd->definedName); - fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, pInvoke, ctx, strResponseData, &result, &error);\n"); + fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, *pMsg->invoke, ctx, strResponseData, &result, &error);\n"); } else { fprintf(src, "\t\tauto invokeResult = pInt->OnInvoke_%s(&argument, &result, ctx);\n", vd->definedName); - fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, pInvoke, ctx, strResponseData, &result);\n"); + fprintf(src, "\t\tlRoseResult = pBase->HandleOnInvokeResult(invokeResult, *pMsg->invoke, ctx, strResponseData, &result);\n"); } } else @@ -1239,7 +1239,7 @@ static void PrintROSEOnInvokeswitchCase(FILE* src, int bEvents, Module* mod, Val const char* pszHandlerPrefix = bEvents ? "RoseEvent" : "RoseInvoke"; fprintf(src, "\tcase OPID_%s:\n", vd->definedName); - fprintf(src, "\t\treturn %s_OPID_%s(pMsg, pInvoke, pBase, pInt, ctx, strResponseData);\n", pszHandlerPrefix, vd->definedName); + fprintf(src, "\t\treturn %s_OPID_%s(pMsg, pBase, pInt, ctx, strResponseData);\n", pszHandlerPrefix, vd->definedName); } } } /* PrintROSEOnInvokeswitchCase */ @@ -2039,8 +2039,6 @@ void PrintChoiceDefCodeBerDecodeContent(FILE* src, FILE* hdr, Module* m, CxxRule } varName = cxxtri->fieldName; - /* set choice id for to this elment */ - fprintf(src, "\t\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); /* alloc elmt if nec */ if (cxxtri->isPtr) @@ -2079,6 +2077,8 @@ void PrintChoiceDefCodeBerDecodeContent(FILE* src, FILE* hdr, Module* m, CxxRule fprintf(src, "%s", getAccessor(cxxtri->isPtr)); fprintf(src, "B%s(_b, tag, elmtLen%d, bytesDecoded);\n", r->decodeContentBaseName, elmtLevel); } + /* set choice id only after the arm decoded successfully */ + fprintf(src, "\t\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); /* decode Eoc (s) */ for (i = elmtLevel - 1; i >= 0; i--) @@ -2096,10 +2096,10 @@ void PrintChoiceDefCodeBerDecodeContent(FILE* src, FILE* hdr, Module* m, CxxRule if (extensionsExist) { fprintf(src, "\t\t\tAsnAny extAny;\n"); - fprintf(src, "\t\t\textension = new AsnExtension;\n"); - fprintf(src, "\t\t\tchoiceId = extensionCid;\n"); fprintf(src, "\t\t\textAny.BDecContent(_b, tag, elmtLen0, bytesDecoded);\n"); + fprintf(src, "\t\t\textension = new AsnExtension;\n"); fprintf(src, "\t\t\textension->extList.insert( extension->extList.end(), extAny );\n"); + fprintf(src, "\t\t\t%s = extensionCid;\n", r->choiceIdFieldName); fprintf(src, "\t\t\tbreak;\n"); } else @@ -2279,16 +2279,19 @@ void PrintChoiceDefCodeJsonDec(FILE* src, FILE* hdr, Module* m, CxxRules* r, Typ fprintf(src, "\telse if (b.isMember(\"%s\"))\n", varName); fprintf(src, "\t{\n"); - fprintf(src, "\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); if (cxxtri->isPtr) { fprintf(src, "\t\tdelete %s;\n", varName); fprintf(src, "\t\t%s = new %s;\n", varName, cxxtri->className); fprintf(src, "\t\tif (!%s->JDec(b[\"%s\"]))\n", varName, varName); + fprintf(src, "\t\t\tthrow InvalidTagException(typeName(), \"decode failed: %s\", STACK_ENTRY);\n", varName); } else + { fprintf(src, "\t\tif (!%s.JDec(b[\"%s\"]))\n", varName, varName); - fprintf(src, "\t\t\tthrow InvalidTagException(typeName(), \"decode failed: %s\", STACK_ENTRY);\n", varName); + fprintf(src, "\t\t\tthrow InvalidTagException(typeName(), \"decode failed: %s\", STACK_ENTRY);\n", varName); + } + fprintf(src, "\t\t%s = %s;\n", r->choiceIdFieldName, cxxtri->choiceIdSymbol); fprintf(src, "\t}\n"); } @@ -4620,11 +4623,11 @@ void PrintROSECode(FILE* src, FILE* hdr, FILE* hdrInterface, ModuleList* mods, M // generate the InvokeHandler fprintf(hdr, "\t// The main Invoke Dispatcher\n"); - fprintf(hdr, "\tstatic long OnInvoke(SNACC::ROSEMessage* pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData);\n", m->ROSEClassName); - fprintf(src, "long %s::OnInvoke(SNACC::ROSEMessage* pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", m->ROSEClassName, m->ROSEClassName); + fprintf(hdr, "\tstatic long OnInvoke(std::unique_ptr& pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData);\n", m->ROSEClassName); + fprintf(src, "long %s::OnInvoke(std::unique_ptr& pMsg, SnaccROSESender* pBase, %sInterface* pInt, SnaccInvokeContext& ctx, std::string& strResponseData)\n", m->ROSEClassName, m->ROSEClassName); fprintf(src, "{\n"); - fprintf(src, "\tauto pInvoke = pMsg->invoke;\n"); - fprintf(src, "\tswitch (pInvoke->operationID)\n"); + fprintf(src, "\tauto& invoke = *pMsg->invoke;\n"); + fprintf(src, "\tswitch (invoke.operationID)\n"); fprintf(src, "\t{\n"); // Rose Interface class diff --git a/cpp-lib/include/SNACCROSE.h b/cpp-lib/include/SNACCROSE.h index 0262932..675de62 100644 --- a/cpp-lib/include/SNACCROSE.h +++ b/cpp-lib/include/SNACCROSE.h @@ -2,7 +2,7 @@ // // SNACCROSE.h - class definitions for ASN.1 module SNACC-ROSE // -// This file was generated by estos esnacc (V7.0.2, 16.04.2026) +// This file was generated by estos esnacc (V7.0.5, 03.07.2026) // based on Coral WinSnacc written by Deepak Gupta // NOTE: This is a machine generated file - editing not recommended // diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 8284ab3..09da43c 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -33,6 +33,7 @@ namespace SNACC class InvokeProblem; class ROSEAuthRequest; class ROSEAuthResult; + class SnaccException; } // namespace SNACC /*! The SnapcROSEPendingOperation class is used to implement the function calls. @@ -57,8 +58,9 @@ class SnaccROSEPendingOperation const unsigned int m_uiOperationID{}; const std::string m_strOperationName; - /*! The answer message. */ - const SNACC::ROSEMessage* m_pAnswerMessage; + /*! Owned inbound response matched to a pending outbound invoke. + Transferred from CompletePendingOperation; destroyed with the pending op. */ + std::unique_ptr m_pAnswerMessage; /*! Error code (one of the ROSE_ error codes. */ long m_lRoseResult; @@ -70,10 +72,8 @@ class SnaccROSEPendingOperation std::shared_ptr m_pTelemetry; /*! Async Operation completed. - Attention: The AnswerMessage will not be copied. - The AnswerMessage must be new allocated and will be deleted - when processed. */ - void CompleteOperation(long lRoseResult, const SNACC::ROSEMessage* pAnswerMessage, size_t stResponseData = 0); + pAnswerMessage transfers ownership of a decoded response when present. */ + void CompleteOperation(long lRoseResult, std::unique_ptr pAnswerMessage = {}, size_t stResponseData = 0); void FinalizeTelemetry(long lFinalRoseResult, std::shared_ptr pctx); /*! Wait for answer received. */ @@ -215,11 +215,11 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; + virtual long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; /* Send a Reject Message. */ - long EncodeReject(SNACC::ROSEReject* preject, std::string& strResponse); - long SendRejectEx(SNACC::ROSEReject* preject, SnaccInvokeContext& ctx); + long EncodeReject(SNACC::ROSEReject* pReject, std::string& strResponse); + long SendRejectEx(SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx); /* * Encodes a reject as repsonse to an invoke @@ -242,7 +242,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; + virtual long EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override; /*! Increment invoke counter Override from SnaccRoseSender*/ @@ -265,48 +265,48 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /** * An invoke that is send to the other side. Should only be called by the ROSE stub itself generated files * - * pinvoke - the invoke payload (it is put into a ROSEMessage in the function) - * result - decoded result payload in case a result response is received - * error - decoded error payload in case an error response is received + * pInvoke - the invoke payload (it is put into a ROSEMessage in the function) + * pResult - decoded result payload in case a result response is received + * pError - decoded error payload in case an error response is received * szOperationName - the operationName (for logging purposes) * iTimeout - the timeout in milliseconds (-1 uses default m_lMaxInvokeWait, 0 returns immediately without waiting for the result) * pCtx - contextual data for the invoke. The caller may keep another shared reference * to inspect changes after the call. */ - virtual long SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override; + virtual long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override; /** * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response * * invokeResult - the result of the OnInvoke method - * pInvoke - the original invoke + * invoke - the original inbound invoke envelope (borrowed from owned ROSEMessage) * ctx - contextual data for the invoke * strResponse - the encoded result to put it on the transport * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) */ - virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke* pInvoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) override; + virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) override; /** * Allows implementers to customize the decoded invoke response before it is handed back * to the caller, for example to propagate connection-specific session data. * * lRoseResult - the current ROSE result code - * pResponseMsg - the raw response message that was received - * result - the decoded result payload in case a result response is received - * error - the decoded error payload in case an error response is received + * responseMsg - owned pending-op response; read-only for callers and overrides + * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) + * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage* pResponseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) override; + virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) override; /** * An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * - * pinvoke - the invoke payload (it is put into a ROSEMessage in the function) + * pInvoke - the invoke payload (it is put into a ROSEMessage in the function) * pCtx - contextual data for the invoke. The caller may keep another shared reference * to inspect changes after the call. */ - virtual long SendEvent(SNACC::ROSEInvoke* pinvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override; + virtual long SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override; /** * Creates the invoke context for inbound and outbound invoke lifecycles. @@ -314,12 +314,12 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback std::shared_ptr CreateInvokeContext(const SnaccInvokeContextInit& init) override; /** - * Decodes an invoke and properly handles logging for it + * Decodes an invoke argument and optionally logs the full message (BER JSON). * - * pInvokeMessage - the invoke message as provided from the other side - * argument - the argument object (Base type pointer, the caller of the provides the proper type) + * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging + * pArgument - the argument object (Base type pointer, the caller of the provides the proper type) */ - long DecodeInvoke(const SNACC::ROSEMessage* pInvokeMessage, SNACC::AsnType* argument) override; + long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* pArgument) override; protected: // Get the length prefix for a given strJson payload @@ -329,7 +329,9 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! The functions and events. The implementation of this functions is contained in the generated code from the esnacc. */ - virtual long OnInvoke(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; + /*! Generated dispatch entry; borrows pMsg for the synchronous handler call. + Ownership remains with OnInvokeMessage until it returns. */ + virtual long OnInvoke(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; /* Function is called when a received data Packet cannot be decoded (invalid Rose Message) * bAlreadyTransportLogged - true if the transport data has already been logged in the transport log (so we do not need to log it again) @@ -343,10 +345,11 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback } /*! The decoded message. - pmessage is new allocated and must be deleted inside this function. + Ownership transfers via move; invoke/event messages move into `OnInvokeMessage()`, + responses move into `CompletePendingOperation()`. returns true if the message was processed. set bAllowInvokes to false, if invokes are not processed. */ - virtual bool OnROSEMessage(SNACC::ROSEMessage* pmessage, bool bAllowInvokes, unsigned long ulMessageSize); + virtual bool OnROSEMessage(std::unique_ptr pMessage, bool bAllowInvokes, unsigned long ulMessageSize); /* * This callback provides telemetry data for processed ROSE messages. @@ -354,14 +357,43 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback void OnInvokeProcessed(std::shared_ptr data) override; private: - /*! The ROSE component messages. - These are called from the OnROSEMessage. - Do not dete the parameters. The functions are called - before the CompleteOperation */ - virtual void OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long lMessageSize); - virtual void OnResultMessage(SNACC::ROSEResult* pResult, unsigned long lMessageSize); - virtual void OnErrorMessage(SNACC::ROSEError* pError, unsigned long lMessageSize); - virtual void OnRejectMessage(SNACC::ROSEReject* pReject, unsigned long lMessageSize); + /*! Inbound invoke/event dispatch; takes ownership of the decoded message. */ + virtual void OnInvokeMessage(std::unique_ptr pMessage, unsigned long lMessageSize); + /*! Telemetry hook for matched inbound result; borrows arm of owned message. */ + virtual void OnResultMessage(const SNACC::ROSEResult& result, unsigned long lMessageSize); + /*! Telemetry hook for matched inbound error; borrows arm of owned message. */ + virtual void OnErrorMessage(const SNACC::ROSEError& error, unsigned long lMessageSize); + /*! Telemetry hook for inbound reject; borrows arm of owned message. */ + virtual void OnRejectMessage(const SNACC::ROSEReject& reject, unsigned long lMessageSize); + + // Snapshot of inbound invoke metadata taken immediately after a successful + // ROSE envelope decode. + struct InboundInvokeRejectContext + { + long m_invokeId = 0; + unsigned int m_uiOperationID = 0; + std::string m_strOperationName; + + static std::optional TryFrom(const SNACC::ROSEMessage& message); + bool CanSendReject() const; + const char* OperationNameCStr() const; + }; + + struct InboundDecodeFailureResult + { + unsigned int uiOperationID = 0; + const char* szOperationName = nullptr; + SnaccTelemetryData::Outcome outcome = SnaccTelemetryData::Outcome::UNHANDLED; + long lTelemetryResult = ROSE_RE_DECODE_FAILED; + }; + + void EmitInboundDecodeFailureTelemetry(unsigned long ulMessageSize, unsigned int uiOperationID, const char* szOperationName, SnaccTelemetryData::Outcome outcome, long lTelemetryResult); + void EmitInboundGenericDecodeFailureTelemetry(unsigned long ulMessageSize); + InboundDecodeFailureResult HandleInboundRoseDecodeFailure(const std::optional& rejectCtx, bool bSendReject, const std::string& strRejectDetails); + void HandleInboundEnvelopeSnaccDecodeFailure(SNACC::TransportEncoding transportEncoding, SNACC::TransportEncoding hookEncoding, const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData, const SNACC::SnaccException& ex, const std::optional& rejectCtx, bool bSendReject, bool bRejectDetailsFromPrettyJson, const char* szMethod); + void HandleInboundJsonParseDecodeFailure(SNACC::TransportEncoding transportEncoding, const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData, const std::string& parseError, const char* szMethod); + void HandleInboundUnknownEncodingDecodeFailure(const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData); + void HandleInboundOuterDecodeFailure(unsigned long ulMessageSize, const char* szException, const char* szMethod, std::optional errorCode = std::nullopt); // The central process wide telemetry callback static inline SnaccTelemetryCallback* m_pTelemetryCallback{}; @@ -375,7 +407,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! The counter for the InvokeIds */ long m_lInvokeCounter{}; /*! The guard for m_bProcessingAllowed and SnaccROSEPendingOperationMap */ - std::mutex m_InternalProtectMutex; + mutable std::mutex m_InternalProtectMutex; /*! If set, the open file where we append log data to. The pointer is created with ConfigureFileLogging() */ FILE* m_pAsnLogFile{}; /*! true if the logfile already contains data, false if not */ @@ -387,16 +419,16 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback SnaccROSEPendingOperation& AddPendingOperation(int invokeID, unsigned int uiOperationID, const char* szOperationName); void RemovePendingOperation(int invokeID); - /*! Pending Operation result has been received. - Attention: The pMessage Objekt will be taken over from the - SnaccROSEPendingOperation Object and deleted in the end. */ - bool CompletePendingOperation(int invokeID, const SNACC::ROSEMessage* pMessage, unsigned long ulMessageSize); + /*! Matches an inbound response to a pending outbound invoke by invokeID. + Transfers pMessage into SnaccROSEPendingOperation when found; otherwise + destroys it when this function returns. */ + bool CompletePendingOperation(int invokeID, std::unique_ptr pMessage, unsigned long ulMessageSize); bool GetPendingOperationTelemetryInfo(int invokeID, unsigned int& uiOperationID, std::string& strOperationName); static long GetRejectResultCode(const SNACC::ROSEReject* pReject); static long GetRejectResultCode(const SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx); - static long DecodeResponse(const SNACC::ROSEMessage* pResponse, SNACC::ROSEResult** ppResult, SNACC::ROSEError** ppError, SnaccInvokeContext& ctx); + static long DecodeResponse(const SNACC::ROSEMessage& response, SNACC::ROSEResult*& pResult, SNACC::ROSEError*& pError, SnaccInvokeContext& ctx); void CompleteAllPendingOperations(); - bool IsProcessingAllowed(); + bool IsProcessingAllowed() const; SnaccROSEPendingOperationMap m_PendingOperations; @@ -417,7 +449,7 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback int GetJsonHeaderLen(const char* lpBytes, unsigned long iLength); // Encodes a value based on the transportencoding and loglevel - std::string GetEncoded(const SNACC::TransportEncoding encoding, SNACC::AsnType* pValue, unsigned long* pUlSize = nullptr); + std::string GetEncoded(const SNACC::TransportEncoding encoding, const SNACC::AsnType* pValue, unsigned long* pUlSize = nullptr); /** * The combined method that takes care about encoding and sending an event or invoke diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index 5074374..45a9ea7 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -209,45 +209,45 @@ class SnaccROSESender /** An invoke that is send to the other side. Should only be called by the ROSE stub itself generated files * * pInvoke - the invoke payload (it is put into a ROSEMessage in the function) - * result - decoded result payload in case a result response is received - * error - decoded error payload in case an error response is received + * pResult - decoded result payload in case a result response is received + * pError - decoded error payload in case an error response is received * szOperationName - the operationName (for logging purposes) * iTimeout - the timeout in milliseconds (-1 uses default m_lMaxInvokeWait, 0 returns immediately without waiting for the result) * pCtx - contextual data for the invoke. The caller may keep another shared reference * to inspect changes after the call. */ - virtual long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) = 0; + virtual long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) = 0; /** * Handles the response payload of the SendInvoke method. Retrieves the result or error from the response * * lRoseResult - the result of the SendInvoke method - * pResponseMsg - the response message as provided by the SendInvoke method - * result - the result object (Base type pointer, the caller of the invoke provides the proper type) - * error - the error object (Base type pointer, the caller of the invoke provides the proper type) - * pCtx - contextual data for the invoke + * responseMsg - owned pending-op response; read-only for callers and overrides + * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) + * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) + * ctx - contextual data for the invoke */ - virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage* pResponseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) = 0; + virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) = 0; /** * Encodes the result or error from an OnInvoke request. Retrieves the result or error from the response * * invokeResult - the result of the OnInvoke method - * pInvoke - the original invoke + * invoke - the original inbound invoke envelope (borrowed from owned ROSEMessage) * ctx - contextual data for the invoke * strResponse - the encoded result to put it on the transport - * result - the result object (Base type pointer, the caller of the invoke provides the proper type) - * error - the error object (Base type pointer, the caller of the invoke provides the proper type) + * pResult - the result object (Base type pointer, the caller of the invoke provides the proper type) + * pError - the error object (Base type pointer, the caller of the invoke provides the proper type) */ - virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke* pInvoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; + virtual long HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) = 0; /** * Decodes an invoke and properly handles logging for it * - * pInvokeMessage - the invoke message as provided from the other side - * argument - the argument object (Base type pointer, the caller of the provides the proper type) + * invokeMessage - inbound invoke ROSEMessage; borrow only for decode/logging + * pArgument - the argument object (Base type pointer, the caller of the provides the proper type) */ - virtual long DecodeInvoke(const SNACC::ROSEMessage* pInvokeMessage, SNACC::AsnType* argument) = 0; + virtual long DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* pArgument) = 0; /** An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * @@ -266,7 +266,7 @@ class SnaccROSESender * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; + virtual long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; /* * Encodes an error as repsonse to an invoke @@ -276,13 +276,13 @@ class SnaccROSESender * strResponse - the encoded response data to send via the transport layer * szSessionID - the SessionID (this propery is filled by subclassing from the concrete class in case we are handling multiple clients via one connection) */ - virtual long EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; + virtual long EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; }; class SnaccScopedInvokeMessage { public: - explicit SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* argument); + explicit SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* pArgument); SnaccScopedInvokeMessage(const SnaccScopedInvokeMessage&) = delete; SnaccScopedInvokeMessage& operator=(const SnaccScopedInvokeMessage&) = delete; SnaccScopedInvokeMessage(SnaccScopedInvokeMessage&&) = delete; diff --git a/cpp-lib/src/SNACCROSE.cpp b/cpp-lib/src/SNACCROSE.cpp index d4f08a9..490fbdf 100644 --- a/cpp-lib/src/SNACCROSE.cpp +++ b/cpp-lib/src/SNACCROSE.cpp @@ -2,7 +2,7 @@ // // SNACCROSE.cpp - class member functions for ASN.1 module SNACC-ROSE // -// This file was generated by estos esnacc (V7.0.2, 16.04.2026) +// This file was generated by estos esnacc (V7.0.5, 03.07.2026) // based on Coral WinSnacc written by Deepak Gupta // NOTE: This is a machine generated file - editing not recommended // @@ -179,14 +179,14 @@ void ROSERejectChoice::BDecContent(const AsnBuf &_b, AsnTag tag, AsnLen elmtLen0 switch (tag) { case MAKE_TAG_ID(UNIV, PRIM, INTEGER_TAG_CODE): - choiceId = invokedIDCid; invokedID = new AsnInt; invokedID->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokedIDCid; break; case MAKE_TAG_ID(UNIV, PRIM, NULLTYPE_TAG_CODE): - choiceId = invokednullCid; invokednull = new AsnNull; invokednull->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokednullCid; break; default: throw InvalidTagException(typeName(), tag, STACK_ENTRY); @@ -239,19 +239,19 @@ bool ROSERejectChoice::JDec(const SJson::Value& b){ if (b.isMember("invokedID")) { - choiceId = invokedIDCid; delete invokedID; invokedID = new AsnInt; if (!invokedID->JDec(b["invokedID"])) throw InvalidTagException(typeName(), "decode failed: invokedID", STACK_ENTRY); + choiceId = invokedIDCid; } else if (b.isMember("invokednull")) { - choiceId = invokednullCid; delete invokednull; invokednull = new AsnNull; if (!invokednull->JDec(b["invokednull"])) throw InvalidTagException(typeName(), "decode failed: invokednull", STACK_ENTRY); + choiceId = invokednullCid; } else throw InvalidTagException(typeName(), "no valid choice member", STACK_ENTRY); @@ -2758,24 +2758,24 @@ void RejectProblem::BDecContent(const AsnBuf &_b, AsnTag tag, AsnLen elmtLen0, A switch (tag) { case MAKE_TAG_ID(CNTX, PRIM, 0): - choiceId = generalProblemCid; generalProblem = new GeneralProblem; generalProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = generalProblemCid; break; case MAKE_TAG_ID(CNTX, PRIM, 1): - choiceId = invokeProblemCid; invokeProblem = new InvokeProblem; invokeProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokeProblemCid; break; case MAKE_TAG_ID(CNTX, PRIM, 2): - choiceId = returnResultProblemCid; returnResultProblem = new ReturnResultProblem; returnResultProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = returnResultProblemCid; break; case MAKE_TAG_ID(CNTX, PRIM, 3): - choiceId = returnErrorProblemCid; returnErrorProblem = new ReturnErrorProblem; returnErrorProblem->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = returnErrorProblemCid; break; default: throw InvalidTagException(typeName(), tag, STACK_ENTRY); @@ -2834,35 +2834,35 @@ bool RejectProblem::JDec(const SJson::Value& b){ if (b.isMember("generalProblem")) { - choiceId = generalProblemCid; delete generalProblem; generalProblem = new GeneralProblem; if (!generalProblem->JDec(b["generalProblem"])) throw InvalidTagException(typeName(), "decode failed: generalProblem", STACK_ENTRY); + choiceId = generalProblemCid; } else if (b.isMember("invokeProblem")) { - choiceId = invokeProblemCid; delete invokeProblem; invokeProblem = new InvokeProblem; if (!invokeProblem->JDec(b["invokeProblem"])) throw InvalidTagException(typeName(), "decode failed: invokeProblem", STACK_ENTRY); + choiceId = invokeProblemCid; } else if (b.isMember("returnResultProblem")) { - choiceId = returnResultProblemCid; delete returnResultProblem; returnResultProblem = new ReturnResultProblem; if (!returnResultProblem->JDec(b["returnResultProblem"])) throw InvalidTagException(typeName(), "decode failed: returnResultProblem", STACK_ENTRY); + choiceId = returnResultProblemCid; } else if (b.isMember("returnErrorProblem")) { - choiceId = returnErrorProblemCid; delete returnErrorProblem; returnErrorProblem = new ReturnErrorProblem; if (!returnErrorProblem->JDec(b["returnErrorProblem"])) throw InvalidTagException(typeName(), "decode failed: returnErrorProblem", STACK_ENTRY); + choiceId = returnErrorProblemCid; } else throw InvalidTagException(typeName(), "no valid choice member", STACK_ENTRY); @@ -3581,24 +3581,24 @@ void ROSEMessage::BDecContent(const AsnBuf &_b, AsnTag tag, AsnLen elmtLen0, Asn switch (tag) { case MAKE_TAG_ID(CNTX, CONS, 1): - choiceId = invokeCid; invoke = new ROSEInvoke; invoke->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = invokeCid; break; case MAKE_TAG_ID(CNTX, CONS, 2): - choiceId = resultCid; result = new ROSEResult; result->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = resultCid; break; case MAKE_TAG_ID(CNTX, CONS, 3): - choiceId = errorCid; error = new ROSEError; error->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = errorCid; break; case MAKE_TAG_ID(CNTX, CONS, 4): - choiceId = rejectCid; reject = new ROSEReject; reject->BDecContent(_b, tag, elmtLen0, bytesDecoded); + choiceId = rejectCid; break; default: throw InvalidTagException(typeName(), tag, STACK_ENTRY); @@ -3657,35 +3657,35 @@ bool ROSEMessage::JDec(const SJson::Value& b){ if (b.isMember("invoke")) { - choiceId = invokeCid; delete invoke; invoke = new ROSEInvoke; if (!invoke->JDec(b["invoke"])) throw InvalidTagException(typeName(), "decode failed: invoke", STACK_ENTRY); + choiceId = invokeCid; } else if (b.isMember("result")) { - choiceId = resultCid; delete result; result = new ROSEResult; if (!result->JDec(b["result"])) throw InvalidTagException(typeName(), "decode failed: result", STACK_ENTRY); + choiceId = resultCid; } else if (b.isMember("error")) { - choiceId = errorCid; delete error; error = new ROSEError; if (!error->JDec(b["error"])) throw InvalidTagException(typeName(), "decode failed: error", STACK_ENTRY); + choiceId = errorCid; } else if (b.isMember("reject")) { - choiceId = rejectCid; delete reject; reject = new ROSEReject; if (!reject->JDec(b["reject"])) throw InvalidTagException(typeName(), "decode failed: reject", STACK_ENTRY); + choiceId = rejectCid; } else throw InvalidTagException(typeName(), "no valid choice member", STACK_ENTRY); diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 704e6a5..61103dc 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -7,6 +7,9 @@ #include #include #include +#include + +std::string getPrettyPrinted(const SJson::Value& value); using namespace SNACC; @@ -24,6 +27,182 @@ namespace } return ""; } + + // Binds a stack-owned ROSEReject into a ROSEMessage for EncodeReject without + // transferring ownership. Detaches in ~dtor so ~ROSEMessage never deletes the + // caller's reject (including on encode exceptions). + class RoseEncodeRejectBorrow final + { + public: + RoseEncodeRejectBorrow(SNACC::ROSEMessage& message, SNACC::ROSEReject& reject) + { + message.choiceId = ROSEMessage::rejectCid; + message.reject = &reject; + m_pMessage = &message; + } + + ~RoseEncodeRejectBorrow() noexcept + { + if (m_pMessage) + m_pMessage->reject = nullptr; + } + + RoseEncodeRejectBorrow(const RoseEncodeRejectBorrow&) = delete; + RoseEncodeRejectBorrow& operator=(const RoseEncodeRejectBorrow&) = delete; + + private: + // ROSEMessage whose reject arm we borrow into for encoding. + SNACC::ROSEMessage* m_pMessage = nullptr; + }; + + // Builds a stack ROSEResult inside a ROSEMessage for outbound EncodeResult. + // Owns encode-side allocations; borrows caller AsnType payload. Detach in + // ~dtor prevents ~ROSEMessage from deleting stack envelopes or caller values. + class RoseEncodeResultEnvelope final + { + public: + RoseEncodeResultEnvelope(unsigned int uiInvokeID, const AsnType* pResult, const wchar_t* szSessionID) + { + m_result.invokeID = uiInvokeID; + m_result.result = new ROSEResultSeq; + m_result.result->resultValue = 0; + m_result.result->result.value = const_cast(pResult); + if (szSessionID) + m_result.sessionID = new UTF8String(szSessionID); + m_message.choiceId = ROSEMessage::resultCid; + m_message.result = &m_result; + } + + ~RoseEncodeResultEnvelope() noexcept + { + detach(); + } + + SNACC::ROSEMessage& message() noexcept + { + return m_message; + } + + RoseEncodeResultEnvelope(const RoseEncodeResultEnvelope&) = delete; + RoseEncodeResultEnvelope& operator=(const RoseEncodeResultEnvelope&) = delete; + + private: + void detach() noexcept + { + if (m_message.result == &m_result) + m_message.result = nullptr; + if (m_result.result) + m_result.result->result.value = nullptr; + } + + // Stack envelope; owns ROSEResultSeq and optional sessionID. + SNACC::ROSEResult m_result; + // Encode view; borrows m_result until detach(). + SNACC::ROSEMessage m_message; + }; + + // Builds a stack ROSEError inside a ROSEMessage for outbound EncodeError. + // Owns encode-side allocations (AsnAny wrapper, optional sessionID); borrows + // caller AsnType payload. Detach in ~dtor prevents ~ROSEMessage from deleting + // stack envelopes or caller values (including on encode exceptions). + class RoseEncodeErrorEnvelope final + { + public: + RoseEncodeErrorEnvelope(unsigned int uiInvokeID, const AsnType* pError, const wchar_t* szSessionID) + { + m_error.invokedID = uiInvokeID; + m_error.error_value = 0; + m_error.error = new AsnAny(); + m_error.error->value = const_cast(pError); + if (szSessionID) + m_error.sessionID = new UTF8String(szSessionID); + m_message.choiceId = ROSEMessage::errorCid; + m_message.error = &m_error; + } + + ~RoseEncodeErrorEnvelope() noexcept + { + detach(); + } + + SNACC::ROSEMessage& message() noexcept + { + return m_message; + } + + RoseEncodeErrorEnvelope(const RoseEncodeErrorEnvelope&) = delete; + RoseEncodeErrorEnvelope& operator=(const RoseEncodeErrorEnvelope&) = delete; + + private: + void detach() noexcept + { + if (m_message.error == &m_error) + m_message.error = nullptr; + if (m_error.error) + m_error.error->value = nullptr; + } + + // Stack envelope; owns AsnAny wrapper and optional sessionID. + SNACC::ROSEError m_error; + // Encode view; borrows m_error until detach(). + SNACC::ROSEMessage m_message; + }; + + // Binds caller-owned ROSEInvoke into a stack ROSEMessage for Send encoding. + // Detaches invoke pointer in ~dtor so ~ROSEMessage does not delete pInvoke. + class ScopedEncodeInvokeBorrow final + { + public: + ScopedEncodeInvokeBorrow(SNACC::ROSEMessage& message, SNACC::ROSEInvoke& invoke) + : m_message(message) + { + m_message.choiceId = ROSEMessage::invokeCid; + m_message.invoke = &invoke; + } + + ~ScopedEncodeInvokeBorrow() noexcept + { + m_message.invoke = nullptr; + } + + ScopedEncodeInvokeBorrow(const ScopedEncodeInvokeBorrow&) = delete; + ScopedEncodeInvokeBorrow& operator=(const ScopedEncodeInvokeBorrow&) = delete; + + private: + // Outbound invoke ROSEMessage being encoded. + SNACC::ROSEMessage& m_message; + }; + + // JSON Send requires operationName on the invoke; adds a temporary name only + // for encoding when the caller did not set one, and removes it on scope exit. + class ScopedInvokeOperationName final + { + public: + ScopedInvokeOperationName(SNACC::ROSEInvoke& invoke, const char* szOperationName) + { + if (!invoke.operationName && szOperationName) + { + m_pInvoke = &invoke; + invoke.operationName = UTF8String::CreateNewFromASCII(szOperationName); + } + } + + ~ScopedInvokeOperationName() noexcept + { + if (m_pInvoke && m_pInvoke->operationName) + { + delete m_pInvoke->operationName; + m_pInvoke->operationName = nullptr; + } + } + + ScopedInvokeOperationName(const ScopedInvokeOperationName&) = delete; + ScopedInvokeOperationName& operator=(const ScopedInvokeOperationName&) = delete; + + private: + // Set only when this scope allocated operationName on the invoke. + SNACC::ROSEInvoke* m_pInvoke = nullptr; + }; } // namespace SnaccInvokeContextInit::SnaccInvokeContextInit(SnaccInvokeDirection direction, SNACC::ROSEInvoke* pInvoke, const char* szOperationName /*= nullptr*/) @@ -43,13 +222,13 @@ std::shared_ptr SnaccInvokeContext::Clone() const return std::shared_ptr(new SnaccInvokeContext(*this)); } -SnaccScopedInvokeMessage::SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* argument) +SnaccScopedInvokeMessage::SnaccScopedInvokeMessage(long invokeID, unsigned int uiOperationID, SNACC::AsnType* pArgument) : m_pInvoke(new SNACC::ROSEInvoke()) { m_pInvoke->invokeID = invokeID; m_pInvoke->operationID = uiOperationID; m_pInvoke->argument = new AsnAny; - m_pInvoke->argument->value = argument; + m_pInvoke->argument->value = pArgument; } SnaccScopedInvokeMessage::~SnaccScopedInvokeMessage() @@ -100,6 +279,156 @@ std::string getPrettyPrinted(const SJson::Value& value) return SJson::writeString(wbuilder, value); } +std::optional SnaccROSEBase::InboundInvokeRejectContext::TryFrom(const SNACC::ROSEMessage& message) +{ + if (message.choiceId != ROSEMessage::invokeCid || !message.invoke) + return std::nullopt; + const auto* pInvoke = message.invoke; + InboundInvokeRejectContext ctx; + ctx.m_invokeId = pInvoke->invokeID; + ctx.m_uiOperationID = pInvoke->operationID; + const char* szName = SnaccRoseOperationLookup::LookUpName(ctx.m_uiOperationID); + if (szName) + ctx.m_strOperationName = szName; + return ctx; +} + +bool SnaccROSEBase::InboundInvokeRejectContext::CanSendReject() const +{ + return m_invokeId != 99999; +} + +const char* SnaccROSEBase::InboundInvokeRejectContext::OperationNameCStr() const +{ + return m_strOperationName.empty() ? nullptr : m_strOperationName.c_str(); +} + +void SnaccROSEBase::EmitInboundDecodeFailureTelemetry(unsigned long ulMessageSize, unsigned int uiOperationID, const char* szOperationName, SnaccTelemetryData::Outcome outcome, long lTelemetryResult) +{ + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized( + SnaccTelemetryData::Direction::INBOUND, + uiOperationID, + szOperationName, + ulMessageSize, + outcome, + SnaccTelemetryData::Stage::INBOUND_DECODE, + SnaccTelemetryData::Reason::DECODE_FAILED, + lTelemetryResult)); +} + +void SnaccROSEBase::EmitInboundGenericDecodeFailureTelemetry(unsigned long ulMessageSize) +{ + EmitInboundDecodeFailureTelemetry(ulMessageSize, 0, nullptr, SnaccTelemetryData::Outcome::UNHANDLED, ROSE_RE_DECODE_FAILED); +} + +SnaccROSEBase::InboundDecodeFailureResult SnaccROSEBase::HandleInboundRoseDecodeFailure(const std::optional& rejectCtx, bool bSendReject, const std::string& strRejectDetails) +{ + InboundDecodeFailureResult result; + if (!rejectCtx) + return result; + result.uiOperationID = rejectCtx->m_uiOperationID; + result.szOperationName = rejectCtx->OperationNameCStr(); + if (!bSendReject || !rejectCtx->CanSendReject()) + return result; + + ROSEReject reject; + if (rejectCtx->m_invokeId) + { + reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; + reject.invokedID.invokedID = new AsnInt(rejectCtx->m_invokeId); + } + else + { + reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; + reject.invokedID.invokednull = new AsnNull; + } + + reject.reject = new RejectProblem(); + reject.reject->choiceId = RejectProblem::invokeProblemCid; + reject.reject->invokeProblem = new InvokeProblem(SNACC::InvokeProblem::mistypedArgument); + reject.details = UTF8String::CreateNewFromASCII(strRejectDetails.c_str()); + + auto pRejectCtx = SnaccInvokeContext::Create( + SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, nullptr, rejectCtx->OperationNameCStr())); + const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); + if (lRejectResult == ROSE_NOERROR) + { + result.outcome = SnaccTelemetryData::Outcome::REJECT; + result.lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; + } + else + result.lTelemetryResult = lRejectResult; + return result; +} + +void SnaccROSEBase::HandleInboundEnvelopeSnaccDecodeFailure( + SNACC::TransportEncoding transportEncoding, + SNACC::TransportEncoding hookEncoding, + const char* lpBytes, + unsigned long ulMessageSize, + bool& bLogTransportData, + const SNACC::SnaccException& ex, + const std::optional& rejectCtx, + bool bSendReject, + bool bRejectDetailsFromPrettyJson, + const char* szMethod) +{ + if (bLogTransportData) + bLogTransportData = LogTransportData(false, transportEncoding, nullptr, lpBytes, ulMessageSize, nullptr, nullptr); + + SJson::Value error; + error["exception"] = ex.what(); + error["method"] = szMethod; + error["error"] = (int)ex.m_errorCode; + const std::string strError = getPrettyPrinted(error); + PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); + OnRoseDecodeError(bLogTransportData, hookEncoding, lpBytes, ulMessageSize, ex.what()); + + const std::string rejectDetails = bRejectDetailsFromPrettyJson ? strError : ex.what(); + const auto failure = HandleInboundRoseDecodeFailure(rejectCtx, bSendReject, rejectDetails); + EmitInboundDecodeFailureTelemetry(ulMessageSize, failure.uiOperationID, failure.szOperationName, failure.outcome, failure.lTelemetryResult); +} + +void SnaccROSEBase::HandleInboundJsonParseDecodeFailure( + SNACC::TransportEncoding transportEncoding, + const char* lpBytes, + unsigned long ulMessageSize, + bool& bLogTransportData, + const std::string& parseError, + const char* szMethod) +{ + if (bLogTransportData) + bLogTransportData = LogTransportData(false, transportEncoding, nullptr, lpBytes, ulMessageSize, nullptr, nullptr); + + SJson::Value error; + error["exception"] = parseError; + error["method"] = szMethod; + const std::string strError = getPrettyPrinted(error); + PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); + OnRoseDecodeError(bLogTransportData, transportEncoding, lpBytes, ulMessageSize, parseError); + EmitInboundGenericDecodeFailureTelemetry(ulMessageSize); +} + +void SnaccROSEBase::HandleInboundUnknownEncodingDecodeFailure(const char* lpBytes, unsigned long ulMessageSize, bool& bLogTransportData) +{ + if (bLogTransportData) + bLogTransportData = LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulMessageSize, nullptr, nullptr); + OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulMessageSize, "unknown encoding"); + EmitInboundGenericDecodeFailureTelemetry(ulMessageSize); +} + +void SnaccROSEBase::HandleInboundOuterDecodeFailure(unsigned long ulMessageSize, const char* szException, const char* szMethod, std::optional errorCode) +{ + SJson::Value error; + error["exception"] = szException; + error["method"] = szMethod; + if (errorCode) + error["error"] = *errorCode; + const std::string strError = getPrettyPrinted(error); + PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); + EmitInboundGenericDecodeFailureTelemetry(ulMessageSize); +} + /* * Searches in haystack for needle but searches not until the null byte in haystack but until reaching limit */ @@ -246,29 +575,24 @@ SnaccROSEPendingOperation::SnaccROSEPendingOperation(long lInvokeID, unsigned in : m_lInvokeID(lInvokeID), m_uiOperationID(uiOperationID), m_strOperationName(szOperationName ? szOperationName : ""), - m_pAnswerMessage(nullptr), m_lRoseResult(0), m_stResponseData(0) { } -SnaccROSEPendingOperation::~SnaccROSEPendingOperation() -{ - if (m_pAnswerMessage) - delete m_pAnswerMessage; -} +SnaccROSEPendingOperation::~SnaccROSEPendingOperation() = default; bool SnaccROSEPendingOperation::WaitForComplete(long ulTimeOut /*= -1*/) { return m_CompletedEvent.waitfor(ulTimeOut); } -void SnaccROSEPendingOperation::CompleteOperation(long lRoseResult, const SNACC::ROSEMessage* pAnswerMessage, size_t stResponseData) +void SnaccROSEPendingOperation::CompleteOperation(long lRoseResult, std::unique_ptr pAnswerMessage, size_t stResponseData) { m_lRoseResult = lRoseResult; m_stResponseData = stResponseData; if (pAnswerMessage) - m_pAnswerMessage = pAnswerMessage; + m_pAnswerMessage = std::move(pAnswerMessage); m_CompletedEvent.signal(); } @@ -355,10 +679,10 @@ void SnaccROSEBase::CompleteAllPendingOperations() std::lock_guard guard(m_InternalProtectMutex); for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++) - it->second->CompleteOperation(ROSE_TE_SHUTDOWN, NULL); + it->second->CompleteOperation(ROSE_TE_SHUTDOWN); } -bool SnaccROSEBase::IsProcessingAllowed() +bool SnaccROSEBase::IsProcessingAllowed() const { std::lock_guard guard(m_InternalProtectMutex); return m_bProcessingAllowed; @@ -383,7 +707,7 @@ void SnaccROSEBase::RemovePendingOperation(int invokeID) m_PendingOperations.erase(it); } -bool SnaccROSEBase::CompletePendingOperation(int invokeID, const SNACC::ROSEMessage* pMessage, unsigned long ulMessageSize) +bool SnaccROSEBase::CompletePendingOperation(int invokeID, std::unique_ptr pMessage, unsigned long ulMessageSize) { std::lock_guard guard(m_InternalProtectMutex); @@ -407,13 +731,9 @@ bool SnaccROSEBase::CompletePendingOperation(int invokeID, const SNACC::ROSEMess break; } - it->second->CompleteOperation(lRoseResult, pMessage, ulMessageSize); + it->second->CompleteOperation(lRoseResult, std::move(pMessage), ulMessageSize); return true; } - else - { - delete pMessage; - } return false; } @@ -497,69 +817,33 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l case SNACC::TransportEncoding::BER: { AsnBuf buffer((const char*)lpBytes, lSize); - ROSEMessage* pmessage = new ROSEMessage; + auto pMessage = std::make_unique(); AsnLen bytesDecoded = 0; + std::optional rejectCtx; try { - pmessage->BDec(buffer, bytesDecoded); + pMessage->BDec(buffer, bytesDecoded); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); + if (bLogTransportData) + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); + + bReturn = OnROSEMessage(std::move(pMessage), false, lSize); } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, lSize, ex.what()); - - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - ROSEReject reject; - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, lSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + SNACC::TransportEncoding::BER, + lpBytes, + lSize, + bLogTransportData, + ex, + rejectCtx, + false, + false, + __FUNCTION__); return true; } - - if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - // pmessage will be deleted inside - bReturn = OnROSEMessage(pmessage, false, lSize); break; } case SNACC::TransportEncoding::JSON: @@ -570,134 +854,65 @@ bool SnaccROSEBase::OnBinaryDataBlockResult(const char* lpBytes, unsigned long l SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + lSize, value)) { - bool bSuccess = false; - ROSEMessage* pmessage = new ROSEMessage; + auto pMessage = std::make_unique(); + std::optional rejectCtx; try { - bSuccess = pmessage->JDec(value); - if (!bSuccess) + if (!pMessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); + + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); + if (bLogTransportData) + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pMessage.get(), &value); + + bReturn = OnROSEMessage(std::move(pMessage), false, lSize); } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, lSize, ex.what()); - - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - ROSEReject reject; - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - reject.reject = new RejectProblem; - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem; - *reject.reject->invokeProblem = InvokeProblem::mistypedArgument; - reject.details = UTF8String::CreateNewFromASCII(strError.c_str()); - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, lSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + m_eTransportEncoding, + lpBytes, + lSize, + bLogTransportData, + ex, + rejectCtx, + false, + true, + __FUNCTION__); return true; } - - if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, pmessage, &value); - - // pmessage will be deleted inside - bReturn = OnROSEMessage(pmessage, false, lSize); } else { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - -#ifdef _DEBUG - std::string strPayLoad; - strPayLoad.assign(lpBytes, lSize); -#endif - SJson::Value error; - error["exception"] = reader.getFormattedErrorMessages(); - error["where"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, lSize, reader.getFormattedErrorMessages()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundJsonParseDecodeFailure( + m_eTransportEncoding, + lpBytes, + lSize, + bLogTransportData, + reader.getFormattedErrorMessages(), + __FUNCTION__); } break; } default: { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, lSize, nullptr, nullptr); - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, lSize, "unknown encoding"); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundUnknownEncodingDecodeFailure(lpBytes, lSize, bLogTransportData); break; } - break; } } catch (const SnaccException& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(lSize, ex.what(), __FUNCTION__, static_cast(ex.m_errorCode)); } catch (const std::exception& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(lSize, ex.what(), __FUNCTION__); } catch (...) { - SJson::Value error; - error["exception"] = "..."; - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, lSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(lSize, "...", __FUNCTION__); } return bReturn; } @@ -717,7 +932,7 @@ int SnaccROSEBase::GetJsonHeaderLen(const char* lpBytes, unsigned long iLength) return iLen; } -std::string SnaccROSEBase::GetEncoded(const SNACC::TransportEncoding encoding, AsnType* pValue, unsigned long* pUlSize /* = nullptr */) +std::string SnaccROSEBase::GetEncoded(const SNACC::TransportEncoding encoding, const AsnType* pValue, unsigned long* pUlSize /* = nullptr */) { std::string strData; if (pUlSize) @@ -790,73 +1005,31 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, case SNACC::TransportEncoding::BER: { AsnBuf buffer((const char*)lpBytes, ulSize); - ROSEMessage* pmessage = new ROSEMessage; + auto pMessage = std::make_unique(); AsnLen bytesDecoded = 0; + std::optional rejectCtx; try { - pmessage->BDec(buffer, bytesDecoded); + pMessage->BDec(buffer, bytesDecoded); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); if (bLogTransportData) LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulSize, nullptr, nullptr); - // pmessage will be deleted inside - OnROSEMessage(pmessage, true, ulSize); + OnROSEMessage(std::move(pMessage), true, ulSize); } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, ex.what()); - - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - // Provide the detail that the argument was non decodable to the client - ROSEReject reject; - reject.reject = new RejectProblem(); - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem(SNACC::InvokeProblem::mistypedArgument); - reject.details = new UTF8String(); - reject.details->setASCII(ex.what()); - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, ulSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + SNACC::TransportEncoding::BER, + lpBytes, + ulSize, + bLogTransportData, + ex, + rejectCtx, + true, + false, + __FUNCTION__); } break; } @@ -869,220 +1042,125 @@ void SnaccROSEBase::OnBinaryDataBlock(const char* lpBytes, unsigned long ulSize, SJson::Reader reader; if (reader.parse((const char*)lpBytes + iHeaderLen, (const char*)lpBytes + ulSize, value)) { - ROSEMessage* pmessage = new ROSEMessage; + auto pMessage = std::make_unique(); + std::optional rejectCtx; try { - if (!pmessage->JDec(value)) + if (!pMessage->JDec(value)) throw InvalidTagException("ROSEMessage", "decode failed: ROSEMessage", STACK_ENTRY); + rejectCtx = InboundInvokeRejectContext::TryFrom(*pMessage); if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pmessage, &value); - // pmessage will be deleted inside - OnROSEMessage(pmessage, true, ulSize); + LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, pMessage.get(), &value); + OnROSEMessage(std::move(pMessage), true, ulSize); } catch (const SnaccException& ex) { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, ulSize, ex.what()); - - unsigned int uiOperationID = 0; - const char* szOperationName = nullptr; - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - if (pmessage->choiceId == ROSEMessage::invokeCid && pmessage->invoke) - { - auto* pInvoke = pmessage->invoke; - uiOperationID = pInvoke->operationID; - szOperationName = SnaccRoseOperationLookup::LookUpName(uiOperationID); - if ((AsnIntType)pInvoke->invokeID != 99999) - { - ROSEReject reject; - if ((AsnIntType)pInvoke->invokeID) - { - reject.invokedID.choiceId = ROSERejectChoice::invokedIDCid; - reject.invokedID.invokedID = new AsnInt(pInvoke->invokeID); - } - else - { - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - } - reject.reject = new RejectProblem; - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem; - *reject.reject->invokeProblem = InvokeProblem::mistypedArgument; - reject.details = UTF8String::CreateNewFromASCII(strError.c_str()); - - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - } - } - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, szOperationName, ulSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); - delete pmessage; + HandleInboundEnvelopeSnaccDecodeFailure( + m_eTransportEncoding, + m_eTransportEncoding, + lpBytes, + ulSize, + bLogTransportData, + ex, + rejectCtx, + true, + true, + __FUNCTION__); } } else { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = reader.getFormattedErrorMessages(); - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - OnRoseDecodeError(bLogTransportData, m_eTransportEncoding, lpBytes, ulSize, reader.getFormattedErrorMessages()); - - ROSEReject reject; - reject.invokedID.choiceId = ROSERejectChoice::invokednullCid; - reject.invokedID.invokednull = new AsnNull; - - reject.reject = new RejectProblem; - reject.reject->choiceId = RejectProblem::invokeProblemCid; - reject.reject->invokeProblem = new InvokeProblem; - *reject.reject->invokeProblem = InvokeProblem::mistypedArgument; - reject.details = UTF8String::CreateNewFromASCII(strError.c_str()); - auto outcome = SnaccTelemetryData::Outcome::UNHANDLED; - long lTelemetryResult = ROSE_RE_DECODE_FAILED; - auto pRejectCtx = SnaccInvokeContext::Create(SnaccInvokeContextInit(SnaccInvokeDirection::INBOUND, nullptr, nullptr)); - const long lRejectResult = SendRejectEx(&reject, *pRejectCtx); - if (lRejectResult == ROSE_NOERROR) - { - outcome = SnaccTelemetryData::Outcome::REJECT; - lTelemetryResult = ROSE_REJECT_MISTYPEDARGUMENT; - } - else - lTelemetryResult = lRejectResult; - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, outcome, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, lTelemetryResult)); + HandleInboundJsonParseDecodeFailure( + m_eTransportEncoding, + lpBytes, + ulSize, + bLogTransportData, + reader.getFormattedErrorMessages(), + __FUNCTION__); } break; } default: { // if we don't know the encoding, we need to log it binary to ensure proper readability in the logs (ensure payload is hex converted) - if (bLogTransportData) - bLogTransportData = LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, lpBytes, ulSize, nullptr, nullptr); - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, "unknown encoding"); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundUnknownEncodingDecodeFailure(lpBytes, ulSize, bLogTransportData); break; } } } catch (const SnaccException& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(ulSize, ex.what(), __FUNCTION__, static_cast(ex.m_errorCode)); } catch (const std::exception& ex) { - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(ulSize, ex.what(), __FUNCTION__); } catch (...) { - SJson::Value error; - error["exception"] = L"..."; - error["method"] = __FUNCTION__; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, 0, nullptr, ulSize, SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::INBOUND_DECODE, SnaccTelemetryData::Reason::DECODE_FAILED, ROSE_RE_DECODE_FAILED)); + HandleInboundOuterDecodeFailure(ulSize, "...", __FUNCTION__); } } -bool SnaccROSEBase::OnROSEMessage(SNACC::ROSEMessage* pMessage, bool bAllowAllInvokes, unsigned long ulMessageSize) +bool SnaccROSEBase::OnROSEMessage(std::unique_ptr pMessage, bool bAllowAllInvokes, unsigned long ulMessageSize) { bool bProcessed = false; switch (pMessage->choiceId) { case ROSEMessage::invokeCid: - if (pMessage->invoke) + { + auto& invoke = *pMessage->invoke; + if (bAllowAllInvokes || m_multithreadInvokeIDs.find(invoke.operationID) != m_multithreadInvokeIDs.end()) { - auto* pInvoke = pMessage->invoke; - if (bAllowAllInvokes || m_multithreadInvokeIDs.find(pInvoke->operationID) != m_multithreadInvokeIDs.end()) - { - if (pInvoke->operationName && pInvoke->operationID == 0) - pInvoke->operationID = SnaccRoseOperationLookup::LookUpID(pInvoke->operationName->getASCII().c_str()); + if (invoke.operationName && invoke.operationID == 0) + invoke.operationID = SnaccRoseOperationLookup::LookUpID(invoke.operationName->getASCII().c_str()); - if (pInvoke->operationID || pInvoke->operationName) - OnInvokeMessage(pMessage, ulMessageSize); + if (invoke.operationID || invoke.operationName) + OnInvokeMessage(std::move(pMessage), ulMessageSize); - bProcessed = true; - } + bProcessed = true; } break; + } case ROSEMessage::resultCid: - if (pMessage->result) - { - OnResultMessage(pMessage->result, ulMessageSize); - // do not intercept anything if pmessage->result->result == nullptr is in place, because the missing invokeID already takes this into account - CompletePendingOperation(pMessage->result->invokeID, pMessage, ulMessageSize); - return true; - } - break; + { + const int invokeID = pMessage->result->invokeID; + OnResultMessage(*pMessage->result, ulMessageSize); + CompletePendingOperation(invokeID, std::move(pMessage), ulMessageSize); + return true; + } case ROSEMessage::errorCid: - if (pMessage->error) - { - OnErrorMessage(pMessage->error, ulMessageSize); - // do not intercept anything if pmessage->error->error == nullptr is in place, because the missing invokeID already takes this into account - CompletePendingOperation(pMessage->error->invokedID, pMessage, ulMessageSize); - return true; - } - break; + { + const int invokeID = pMessage->error->invokedID; + OnErrorMessage(*pMessage->error, ulMessageSize); + CompletePendingOperation(invokeID, std::move(pMessage), ulMessageSize); + return true; + } case ROSEMessage::rejectCid: - if (pMessage->reject) + { + auto& reject = *pMessage->reject; + OnRejectMessage(reject, ulMessageSize); + if (reject.invokedID.choiceId == ROSERejectChoice::invokedIDCid && reject.invokedID.invokedID != nullptr) { - OnRejectMessage(pMessage->reject, ulMessageSize); - if (pMessage->reject->invokedID.choiceId == ROSERejectChoice::invokedIDCid) - { - // Test! with REJECT the InvokeID is a choice and therefore the ID itself is a pointer! and it can become nullptr. - if (pMessage->reject->invokedID.invokedID != nullptr) - { - CompletePendingOperation(*pMessage->reject->invokedID.invokedID, pMessage, ulMessageSize); - return true; - } - } - bProcessed = true; + const int invokeID = *reject.invokedID.invokedID; + CompletePendingOperation(invokeID, std::move(pMessage), ulMessageSize); + return true; } + bProcessed = true; break; + } default: break; } - delete pMessage; return bProcessed; } -long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* preject, std::string& strResponse) +long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* pReject, std::string& strResponse) { long lRoseResult = ROSE_NOERROR; ROSEMessage rejectMsg; - rejectMsg.choiceId = ROSEMessage::rejectCid; - rejectMsg.reject = preject; + const RoseEncodeRejectBorrow rejectBorrow(rejectMsg, *pReject); // encode now. if (m_eTransportEncoding == SNACC::TransportEncoding::BER) @@ -1123,16 +1201,13 @@ long SnaccROSEBase::EncodeReject(SNACC::ROSEReject* preject, std::string& strRes throw std::runtime_error("invalid m_eTransportEncoding"); } - // prevent delete of preject... - rejectMsg.reject = 0; - return lRoseResult; } -long SnaccROSEBase::SendRejectEx(SNACC::ROSEReject* preject, SnaccInvokeContext& ctx) +long SnaccROSEBase::SendRejectEx(SNACC::ROSEReject* pReject, SnaccInvokeContext& ctx) { std::string strResponse; - auto lResult = EncodeReject(preject, strResponse); + auto lResult = EncodeReject(pReject, strResponse); if (lResult) return lResult; @@ -1227,16 +1302,16 @@ long SnaccROSEBase::EncodeInvokeRejectResponse(const SNACC::ROSEInvoke* pInvoke, return lEncodeResult == ROSE_NOERROR ? lProtocolResult : lEncodeResult; } -void SnaccROSEBase::OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long ulMessageSize) +void SnaccROSEBase::OnInvokeMessage(std::unique_ptr pMessage, unsigned long ulMessageSize) { std::string strResponse; long lResult = ROSE_REJECT_UNKNOWNOPERATION; - auto* pInvoke = pMessage->invoke; - const char* szOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); + auto& invoke = *pMessage->invoke; + const char* szOperationName = SnaccRoseOperationLookup::LookUpName(invoke.operationID); - SnaccInvokeContextInit init(SnaccInvokeDirection::INBOUND, pInvoke, szOperationName); + SnaccInvokeContextInit init(SnaccInvokeDirection::INBOUND, &invoke, szOperationName); auto pCtx = CreateInvokeContext(init); - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::INBOUND, pInvoke->operationID, szOperationName, ulMessageSize); + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::INBOUND, invoke.operationID, szOperationName, ulMessageSize); auto telemetryResult = SnaccTelemetryData::Outcome::UNHANDLED; auto telemetryReason = SnaccTelemetryData::Reason::UNKNOWN_FAILURE; bool bInvokeException = false; @@ -1258,20 +1333,20 @@ void SnaccROSEBase::OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long error["exception"] = ex.what(); error["method"] = __FUNCTION__; error["error"] = (int)ex.m_errorCode; - error["invokeID"] = (int)pInvoke->invokeID; + error["invokeID"] = (int)invoke.invokeID; std::string jsonString = getPrettyPrinted(error); PrintJSONToLog(false, true, nullptr, jsonString.c_str(), jsonString.length()); } } - const bool bIsInvoke = (AsnIntType)pInvoke->invokeID != 99999; + const bool bIsInvoke = (AsnIntType)invoke.invokeID != 99999; const bool bIsRejectResponse = lResult != ROSE_NOERROR && bIsInvoke; // if the Result is ROSE_NOERROR the request has been processed with a result or an error (the invoke context points out if it was replied with an error) // if the result is ROSE_REJECT_ASYNCOPERATION, the result will be sent async if (bIsRejectResponse) { - lResult = EncodeInvokeRejectResponse(pInvoke, lResult, *pCtx, strResponse); + lResult = EncodeInvokeRejectResponse(&invoke, lResult, *pCtx, strResponse); if (lResult != ROSE_NOERROR) telemetryReason = GetUnhandledReasonFromResult(lResult); } @@ -1321,29 +1396,29 @@ void SnaccROSEBase::OnInvokeMessage(SNACC::ROSEMessage* pMessage, unsigned long OnInvokeProcessed(telemetry); } -void SnaccROSEBase::OnResultMessage(SNACC::ROSEResult* presult, unsigned long ulMessageSize) +void SnaccROSEBase::OnResultMessage(const SNACC::ROSEResult& result, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; - GetPendingOperationTelemetryInfo(presult->invokeID, uiOperationID, strOperationName); + GetPendingOperationTelemetryInfo(result.invokeID, uiOperationID, strOperationName); OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::RESULT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_RESULT, ROSE_NOERROR)); } -void SnaccROSEBase::OnErrorMessage(SNACC::ROSEError* perror, unsigned long ulMessageSize) +void SnaccROSEBase::OnErrorMessage(const SNACC::ROSEError& error, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; - GetPendingOperationTelemetryInfo(perror->invokedID, uiOperationID, strOperationName); + GetPendingOperationTelemetryInfo(error.invokedID, uiOperationID, strOperationName); OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::ERR, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_ERROR, ROSE_ERROR_VALUE)); } -void SnaccROSEBase::OnRejectMessage(SNACC::ROSEReject* preject, unsigned long ulMessageSize) +void SnaccROSEBase::OnRejectMessage(const SNACC::ROSEReject& reject, unsigned long ulMessageSize) { unsigned int uiOperationID = 0; std::string strOperationName; - if (preject->invokedID.choiceId == ROSERejectChoice::invokedIDCid && preject->invokedID.invokedID != nullptr) - GetPendingOperationTelemetryInfo(*preject->invokedID.invokedID, uiOperationID, strOperationName); - OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::REJECT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_REJECT, GetRejectResultCode(preject))); + if (reject.invokedID.choiceId == ROSERejectChoice::invokedIDCid && reject.invokedID.invokedID != nullptr) + GetPendingOperationTelemetryInfo(*reject.invokedID.invokedID, uiOperationID, strOperationName); + OnInvokeProcessed(SnaccTelemetryData::CreateFinalized(SnaccTelemetryData::Direction::INBOUND, uiOperationID, strOperationName.c_str(), ulMessageSize, SnaccTelemetryData::Outcome::REJECT, SnaccTelemetryData::Stage::INBOUND_RESPONSE, SnaccTelemetryData::Reason::REMOTE_REJECT, GetRejectResultCode(&reject))); } long SnaccROSEBase::GetNextInvokeID() @@ -1364,8 +1439,7 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName *pstRequestData = 0; ROSEMessage invokeMsg; - invokeMsg.choiceId = ROSEMessage::invokeCid; - invokeMsg.invoke = pInvoke; + const ScopedEncodeInvokeBorrow invokeBorrow(invokeMsg, *pInvoke); if (m_eTransportEncoding == SNACC::TransportEncoding::BER) { @@ -1379,9 +1453,7 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName } else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) { - // The mobiles currently rely on the operationName so we need to fill it if it is missing here - if (!pInvoke->operationName) - pInvoke->operationName = UTF8String::CreateNewFromASCII(szOperationName); + const ScopedInvokeOperationName operationName(*pInvoke, szOperationName); std::string strData = GetEncoded(m_eTransportEncoding, &invokeMsg); @@ -1405,25 +1477,22 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName throw std::runtime_error("invalid m_eTransportEncoding"); } - // prevent autodelete of pInvoke - invokeMsg.invoke = NULL; - return lRoseResult; } -long SnaccROSEBase::SendEvent(SNACC::ROSEInvoke* pinvoke, const char* szOperationName, std::shared_ptr pCtx /*= {}*/) +long SnaccROSEBase::SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx /*= {}*/) { const auto chronoCreated = std::chrono::steady_clock::now(); - const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pinvoke->operationID); + const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); if (!pCtx) { - SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pinvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); + SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pInvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); pCtx = CreateInvokeContext(init); } auto& ctx = *pCtx; size_t stRequestData = 0; - const long lRoseResult = IsProcessingAllowed() ? Send(pinvoke, szOperationName, ctx, &stRequestData) : ROSE_TE_SHUTDOWN; - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); + const long lRoseResult = IsProcessingAllowed() ? Send(pInvoke, szOperationName, ctx, &stRequestData) : ROSE_TE_SHUTDOWN; + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); telemetry->finalize(lRoseResult == ROSE_NOERROR ? SnaccTelemetryData::Outcome::EVENT : SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, lRoseResult == ROSE_NOERROR ? SnaccTelemetryData::Reason::LOCAL_EVENT : GetUnhandledReasonFromResult(lRoseResult), lRoseResult, std::nullopt, pCtx); OnInvokeProcessed(telemetry); return lRoseResult; @@ -1514,32 +1583,32 @@ bool SnaccROSEBase::LogTransportData(const bool bOutbound, const SNACC::Transpor return bTransportDataWasLogged; } -long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName /*= nullptr*/, int iTimeout /*= -1*/, std::shared_ptr pCtx /*= {}*/) +long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName /*= nullptr*/, int iTimeout /*= -1*/, std::shared_ptr pCtx /*= {}*/) { const auto chronoCreated = std::chrono::steady_clock::now(); - const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pinvoke->operationID); + const char* szResolvedOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); // Ensure that we always have a ctx if (!pCtx) { - SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pinvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); + SnaccInvokeContextInit init(SnaccInvokeDirection::OUTBOUND, pInvoke, szResolvedOperationName ? szResolvedOperationName : szOperationName); pCtx = CreateInvokeContext(init); } auto& ctx = *pCtx; if (!IsProcessingAllowed()) { - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, 0, chronoCreated); + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::SHUTDOWN, ROSE_TE_SHUTDOWN, std::nullopt, std::move(pCtx)); OnInvokeProcessed(telemetry); return ROSE_TE_SHUTDOWN; } - auto& pendingOP = AddPendingOperation(pinvoke->invokeID, pinvoke->operationID, szResolvedOperationName); + auto& pendingOP = AddPendingOperation(pInvoke->invokeID, pInvoke->operationID, szResolvedOperationName); size_t stRequestData = 0; - long lRoseResult = Send(pinvoke, szOperationName, ctx, &stRequestData); - pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); + long lRoseResult = Send(pInvoke, szOperationName, ctx, &stRequestData); + pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); if (lRoseResult == 0) { @@ -1574,7 +1643,7 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* resul pendingOP.m_lRoseResult = lRoseResult; if (pendingOP.m_pAnswerMessage) - lRoseResult = HandleInvokeResult(lRoseResult, pendingOP.m_pAnswerMessage, result, error, ctx); + lRoseResult = HandleInvokeResult(lRoseResult, *pendingOP.m_pAnswerMessage, pResult, pError, ctx); pendingOP.FinalizeTelemetry(lRoseResult, pCtx); OnInvokeProcessed(pendingOP.m_pTelemetry); @@ -1584,13 +1653,13 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* resul return lRoseResult; } -long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage* pResponse, SNACC::ROSEResult** ppResult, SNACC::ROSEError** ppError, SnaccInvokeContext& ctx) +long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage& response, SNACC::ROSEResult*& pResult, SNACC::ROSEError*& pError, SnaccInvokeContext& ctx) { long lRoseResult = ROSE_RE_INVALID_ANSWER; - if (!pResponse) - return lRoseResult; + pResult = nullptr; + pError = nullptr; - switch (pResponse->choiceId) + switch (response.choiceId) { case ROSEMessage::notinitialized: throw std::runtime_error("response ROSEMessage choiceId is notinitialized"); @@ -1598,149 +1667,108 @@ long SnaccROSEBase::DecodeResponse(const SNACC::ROSEMessage* pResponse, SNACC::R break; case ROSEMessage::resultCid: lRoseResult = ROSE_NOERROR; - if (pResponse->result && ppResult) - *ppResult = pResponse->result; + pResult = response.result; break; case ROSEMessage::errorCid: lRoseResult = ROSE_ERROR_VALUE; - if (pResponse->error && ppError) - *ppError = pResponse->error; + pError = response.error; break; case ROSEMessage::rejectCid: - lRoseResult = GetRejectResultCode(pResponse->reject, ctx); + lRoseResult = GetRejectResultCode(response.reject, ctx); break; } return lRoseResult; } -long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) +long SnaccROSEBase::EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) { long lRoseResult = ROSE_NOERROR; + RoseEncodeResultEnvelope encode(uiInvokeID, pResult, szSessionID); - ROSEResult result; - result.invokeID = uiInvokeID; - result.result = new ROSEResultSeq; - result.result->resultValue = 0; - result.result->result.value = pResult; - if (szSessionID) - result.sessionID = new UTF8String(szSessionID); - + if (m_eTransportEncoding == SNACC::TransportEncoding::BER) { - ROSEMessage ResultMsg; - ResultMsg.choiceId = ROSEMessage::resultCid; - ResultMsg.result = &result; - - // encode now. - if (m_eTransportEncoding == SNACC::TransportEncoding::BER) - { - AsnBuf OutBuf; - AsnLen BytesEncoded = ResultMsg.BEnc(OutBuf); - - OutBuf.ResetMode(); - OutBuf.GetSeg(strResponse, BytesEncoded); + AsnBuf OutBuf; + AsnLen BytesEncoded = encode.message().BEnc(OutBuf); - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), BytesEncoded, &ResultMsg, nullptr); - } - else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) - { - auto value = ResultMsg.JEnc(); + OutBuf.ResetMode(); + OutBuf.GetSeg(strResponse, BytesEncoded); - int logLevel = (int)GetLogLevel(true); - if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) - strResponse = getPrettyPrinted(value); - else - { - SJson::FastWriter writer; - strResponse = writer.write(value); - } + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), BytesEncoded, &encode.message(), nullptr); + } + else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) + { + auto value = encode.message().JEnc(); - std::string strPrefix; - if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) - lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); - if (lRoseResult == ROSE_NOERROR) - { - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &ResultMsg, nullptr); - if (!strPrefix.empty()) - strResponse.insert(0, strPrefix); - } - } + int logLevel = (int)GetLogLevel(true); + if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) + strResponse = getPrettyPrinted(value); else { - throw std::runtime_error("invalid m_eTransportEncoding"); + SJson::FastWriter writer; + strResponse = writer.write(value); } - // prevent delete of presult... - ResultMsg.result = nullptr; + std::string strPrefix; + if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) + lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); + if (lRoseResult == ROSE_NOERROR) + { + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &encode.message(), nullptr); + if (!strPrefix.empty()) + strResponse.insert(0, strPrefix); + } + } + else + { + throw std::runtime_error("invalid m_eTransportEncoding"); } - - // prevent delete of value... - result.result->result.value = nullptr; return lRoseResult; } -long SnaccROSEBase::EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) +long SnaccROSEBase::EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID /*= nullptr*/) { long lRoseResult = ROSE_NOERROR; + RoseEncodeErrorEnvelope encode(uiInvokeID, pError, szSessionID); - ROSEError error; - error.invokedID = uiInvokeID; - error.error_value = 0; - error.error = new AsnAny(); - error.error->value = pError; - if (szSessionID) - error.sessionID = new UTF8String(szSessionID); - + if (m_eTransportEncoding == SNACC::TransportEncoding::BER) { - ROSEMessage errorMsg; - errorMsg.choiceId = ROSEMessage::errorCid; - errorMsg.error = &error; + AsnBuf OutBuf; + AsnLen BytesEncoded = encode.message().BEnc(OutBuf); - // encode now. - if (m_eTransportEncoding == SNACC::TransportEncoding::BER) - { - AsnBuf OutBuf; - AsnLen BytesEncoded = errorMsg.BEnc(OutBuf); + OutBuf.ResetMode(); + OutBuf.GetSeg(strResponse, BytesEncoded); - OutBuf.ResetMode(); - OutBuf.GetSeg(strResponse, BytesEncoded); + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &encode.message(), nullptr); + } + else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) + { + auto value = encode.message().JEnc(); - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &errorMsg, nullptr); - } - else if (m_eTransportEncoding == SNACC::TransportEncoding::JSON || m_eTransportEncoding == SNACC::TransportEncoding::JSON_NO_HEADING) + int logLevel = (int)GetLogLevel(true); + if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) + strResponse = getPrettyPrinted(value); + else { - auto value = errorMsg.JEnc(); - - int logLevel = (int)GetLogLevel(true); - if (logLevel & (int)EAsnLogLevel::JSON || logLevel & (int)EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED) - strResponse = getPrettyPrinted(value); - else - { - SJson::FastWriter writer; - strResponse = writer.write(value); - } - - std::string strPrefix; - if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) - lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); - if (lRoseResult == ROSE_NOERROR) - { - LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &errorMsg, nullptr); - if (!strPrefix.empty()) - strResponse.insert(0, strPrefix); - } + SJson::FastWriter writer; + strResponse = writer.write(value); } - else + + std::string strPrefix; + if (m_eTransportEncoding == SNACC::TransportEncoding::JSON) + lRoseResult = GetJsonLengthPrefix(strResponse, strPrefix); + if (lRoseResult == ROSE_NOERROR) { - throw std::runtime_error("invalid m_eTransportEncoding"); + LogTransportData(true, m_eTransportEncoding, nullptr, strResponse.c_str(), strResponse.length(), &encode.message(), nullptr); + if (!strPrefix.empty()) + strResponse.insert(0, strPrefix); } - // prevent delete of perror... - errorMsg.error = nullptr; } - - // prevent delete of value... - error.error->value = nullptr; + else + { + throw std::runtime_error("invalid m_eTransportEncoding"); + } return lRoseResult; } @@ -1772,26 +1800,26 @@ SNACC::TransportEncoding SnaccROSEBase::GetTransportEncoding() const return m_eTransportEncoding; } -long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage* pResponseMsg, SNACC::AsnType* result, SNACC::AsnType* error, SnaccInvokeContext& ctx) +long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) { // In case of transport errors, we hand that value back as we have no response to parse if (ISROSE_TE(lRoseResult)) return lRoseResult; - SNACC::ROSEError* pError = nullptr; - SNACC::ROSEResult* pResult = nullptr; - lRoseResult = DecodeResponse(pResponseMsg, &pResult, &pError, ctx); + SNACC::ROSEError* pRoseError = nullptr; + SNACC::ROSEResult* pRoseResult = nullptr; + lRoseResult = DecodeResponse(responseMsg, pRoseResult, pRoseError, ctx); if (lRoseResult == ROSE_NOERROR) { - if (pResult && pResult->result && result) + if (pRoseResult && pRoseResult->result && pResult) { AsnLen len; try { - if (pResult->result->result.anyBuf) + if (pRoseResult->result->result.anyBuf) { - result->BDec(*pResult->result->result.anyBuf, len); + pResult->BDec(*pRoseResult->result->result.anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // @@ -1801,24 +1829,28 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { + // LogTransportData needs the decoded payload inside the ROSE tree for JEnc(). + // Graft temporarily, then restore below — the owned message is unchanged afterward. + // In-place graft avoids copying ROSEMessage and duplicating the BER anyBuf. + auto& logMsg = const_cast(responseMsg); // Backup the original response object inside the response message - ROSEResultSeq* pOriginalResponse = pResponseMsg->result->result; + ROSEResultSeq* pOriginalResponse = logMsg.result->result; // Set the decoded result object into the response to be able to fully log the whole message - pResponseMsg->result->result = new ROSEResultSeq(); - pResponseMsg->result->result->result.value = result; - LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, pResponseMsg, nullptr); + logMsg.result->result = new ROSEResultSeq(); + logMsg.result->result->result.value = pResult; + LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &logMsg, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - pResponseMsg->result->result->result.value = NULL; + logMsg.result->result->result.value = NULL; // Delete the inserted result object and reset to the original response object - delete pResponseMsg->result->result; - pResponseMsg->result->result = pOriginalResponse; + delete logMsg.result->result; + logMsg.result->result = pOriginalResponse; } } - else if (pResult->result->result.jsonBuf) + else if (pRoseResult->result->result.jsonBuf) { - if (!result->JDec(*pResult->result->result.jsonBuf)) + if (!pResult->JDec(*pRoseResult->result->result.jsonBuf)) lRoseResult = ROSE_RE_DECODE_FAILED; // No logging here as the full object has already been logged in OnBinaryDataBlock } @@ -1838,14 +1870,14 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag } else if (lRoseResult == ROSE_ERROR_VALUE) { - if (pError && pError->error && error) + if (pRoseError && pRoseError->error && pError) { AsnLen len; try { - if (pError->error->anyBuf) + if (pRoseError->error->anyBuf) { - error->BDec(*pError->error->anyBuf, len); + pError->BDec(*pRoseError->error->anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // @@ -1855,24 +1887,28 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { + // LogTransportData needs the decoded payload inside the ROSE tree for JEnc(). + // Graft temporarily, then restore below — the owned message is unchanged afterward. + // In-place graft avoids copying ROSEMessage and duplicating the BER anyBuf. + auto& logMsg = const_cast(responseMsg); // Backup the original error object inside the response message - AsnAny* pOriginalError = pResponseMsg->error->error; + AsnAny* pOriginalError = logMsg.error->error; // Set the decoded error object into the response to be able to fully log the whole message - pResponseMsg->error->error = new AsnAny(); - pResponseMsg->error->error->value = error; - LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, pResponseMsg, nullptr); + logMsg.error->error = new AsnAny(); + logMsg.error->error->value = pError; + LogTransportData(false, SNACC::TransportEncoding::BER, nullptr, nullptr, 0, &logMsg, nullptr); // As we hand back the error object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - pResponseMsg->error->error->value = NULL; + logMsg.error->error->value = NULL; // Delete the inserted error object and reset to the original response object - delete pResponseMsg->error->error; - pResponseMsg->error->error = pOriginalError; + delete logMsg.error->error; + logMsg.error->error = pOriginalError; } } - else if (pError->error->jsonBuf) + else if (pRoseError->error->jsonBuf) { - if (!error->JDec(*pError->error->jsonBuf)) + if (!pError->JDec(*pRoseError->error->jsonBuf)) lRoseResult = ROSE_RE_DECODE_FAILED; // No logging here as the full object has already been logged in OnBinaryDataBlock } @@ -1894,9 +1930,9 @@ long SnaccROSEBase::HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessag return lRoseResult; } -long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke* pInvoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) +long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const SNACC::ROSEInvoke& invoke, SnaccInvokeContext& ctx, std::string& strResponse, SNACC::AsnType* pResult, SNACC::AsnType* pError) { - if (pInvoke && (AsnIntType)pInvoke->invokeID == 99999) + if ((AsnIntType)invoke.invokeID == 99999) { strResponse.clear(); ctx.m_bResponseIsError = false; @@ -1906,24 +1942,21 @@ long SnaccROSEBase::HandleOnInvokeResult(SNACC::InvokeResult invokeResult, const switch (invokeResult) { case InvokeResult::returnResult: - return EncodeResult(pInvoke->invokeID, pResult, strResponse); + return EncodeResult(invoke.invokeID, pResult, strResponse); case InvokeResult::returnError: ctx.m_bResponseIsError = true; - return EncodeError(pInvoke->invokeID, pError, strResponse); + return EncodeError(invoke.invokeID, pError, strResponse); default: return ctx.m_lRejectResult ? ctx.m_lRejectResult : ROSE_REJECT_FUNCTIONMISSING; } } -long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage* pMessage, SNACC::AsnType* argument) +long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage& invokeMessage, SNACC::AsnType* pArgument) { - if (!pMessage || !pMessage->invoke) - return ROSE_RE_DECODE_FAILED; - - auto* pInvoke = pMessage->invoke; - if (!pInvoke->argument) + auto& invoke = *invokeMessage.invoke; + if (!invoke.argument) { - if (argument->mayBeEmpty()) + if (pArgument->mayBeEmpty()) return ROSE_NOERROR; else return ROSE_REJECT_ARGUMENT_MISSING; @@ -1933,48 +1966,53 @@ long SnaccROSEBase::DecodeInvoke(const SNACC::ROSEMessage* pMessage, SNACC::AsnT try { - if (pInvoke->argument->anyBuf) + if (invoke.argument->anyBuf) { AsnLen len = 0; - argument->BDec(*pInvoke->argument->anyBuf, len); + pArgument->BDec(*invoke.argument->anyBuf, len); // Special to log the *full* ROSE Message in json // While for JSON Transport we can simply decode the full message on BER we need to decode the envelop and the payload // // We log the plain received BER payload in OnBinaryDataBlock and the fully decoded message here - // To be able to do this we need to put the decoded payload (here result) into the pResponseMsg and then log it + // To be able to do this we need to put the decoded payload (here result) into the response message and then log it // We only do this if JSON logging is enabled int logLevel = (int)GetLogLevel(false); if (logLevel & ((int)SNACC::EAsnLogLevel::JSON | (int)SNACC::EAsnLogLevel::JSON_ALWAYS_PRETTY_PRINTED)) { + // LogTransportData needs the decoded payload inside the ROSE tree for JEnc(). + // Graft temporarily, then restore below — the owned message is unchanged afterward. + // In-place graft avoids copying ROSEMessage and duplicating the BER anyBuf. + auto& logMsg = const_cast(invokeMessage); + auto& logInvoke = *logMsg.invoke; // Backup the original response object inside the response message - AsnAny* pOriginalArgument = pInvoke->argument; + AsnAny* pOriginalArgument = logInvoke.argument; // Set the decoded result object into the response to be able to fully log the whole message - pInvoke->argument = new AsnAny(); - pInvoke->argument->value = argument; + logInvoke.argument = new AsnAny(); + logInvoke.argument->value = pArgument; // Get the name of the called operation for logging std::string strOperationName; const char* szOperationName = nullptr; - if (pInvoke->operationName) + if (logInvoke.operationName) { - strOperationName = pInvoke->operationName->getUTF8(); + strOperationName = logInvoke.operationName->getUTF8(); szOperationName = strOperationName.c_str(); } if (!szOperationName) - szOperationName = SnaccRoseOperationLookup::LookUpName(pInvoke->operationID); - LogTransportData(false, SNACC::TransportEncoding::BER, szOperationName, nullptr, 0, pMessage, nullptr); + szOperationName = SnaccRoseOperationLookup::LookUpName(logInvoke.operationID); + LogTransportData(false, SNACC::TransportEncoding::BER, szOperationName, nullptr, 0, &logMsg, nullptr); // As we hand back the result object to the outer world (function argument) we need to set it to NULL to prevent deletion if we discard the inserted object - pInvoke->argument->value = NULL; + logInvoke.argument->value = NULL; // Delete the inserted result object and reset to the original response object - delete pInvoke->argument; - pInvoke->argument = pOriginalArgument; + delete logInvoke.argument; + logInvoke.argument = pOriginalArgument; } } - else if (pInvoke->argument->jsonBuf) + else if (invoke.argument->jsonBuf) { - argument->JDec(*pInvoke->argument->jsonBuf); + pArgument->JDec(*invoke.argument->jsonBuf); // No logging here as the full object has already been logged in OnBinaryDataBlock } else diff --git a/cpp-lib/tests/CMakeLists.txt b/cpp-lib/tests/CMakeLists.txt index 6bf7f20..158d155 100644 --- a/cpp-lib/tests/CMakeLists.txt +++ b/cpp-lib/tests/CMakeLists.txt @@ -52,6 +52,7 @@ target_link_libraries(snacc-sample-generated PUBLIC snacc_cpp_lib) add_executable(cpp-lib-sample-runtime-tests call_flow_tests.cpp + invoke_context_tests.cpp logging_tests.cpp logical_failure_tests.cpp public_api_tests.cpp diff --git a/cpp-lib/tests/invoke_context_tests.cpp b/cpp-lib/tests/invoke_context_tests.cpp new file mode 100644 index 0000000..e2e6301 --- /dev/null +++ b/cpp-lib/tests/invoke_context_tests.cpp @@ -0,0 +1,702 @@ +#include "test_support/sample_runtime_harness.h" + +#include +#include + +namespace sample_runtime_tests +{ + namespace + { + const SessionInvokeContext* AsSessionContext(const SnaccInvokeContext* pContext) + { + return dynamic_cast(pContext); + } + + const SnaccTelemetryData* FindOutboundWaitTelemetry(const std::vector>& entries, const SnaccTelemetryData::Reason reason) + { + const auto it = std::find_if(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { return entry && entry->m_Direction == SnaccTelemetryData::Direction::OUTBOUND && entry->m_Stage == SnaccTelemetryData::Stage::OUTBOUND_WAIT && entry->m_Reason == reason; }); + return it == entries.end() ? nullptr : it->get(); + } + + bool HasInboundResponseTelemetryForOperation(const std::vector>& entries, const unsigned int operationId) + { + return std::any_of(entries.begin(), entries.end(), [&](const std::shared_ptr& entry) { return entry && entry->m_Direction == SnaccTelemetryData::Direction::INBOUND && entry->m_Stage == SnaccTelemetryData::Stage::INBOUND_RESPONSE && entry->m_uiOperationID == operationId; }); + } + } // namespace + + // Specifies custom SnaccInvokeContext lifecycle for inbound and outbound ROSE paths. + class InvokeContextRuntimeTest : public RuntimeTestBase + { + protected: + void AssertInboundHandlerReceivesCustomInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& handlerSnapshot = ServerInboundObservation().HandlerSnapshot(); + ASSERT_TRUE(handlerSnapshot.WasCaptured()); + EXPECT_TRUE(handlerSnapshot.IsSessionContext()) << "inbound handler must receive CreateInvokeContext() product"; + EXPECT_EQ("server-session", handlerSnapshot.LocalSessionId()); + EXPECT_EQ("client-session", handlerSnapshot.InvokeSessionId()); + } + + void AssertInboundResponseSendReusesHandlerInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "inbound response send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundHandlerRejectReusesInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.implementGetSettings = false; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "inbound reject send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundApplicationErrorReusesInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.getSettingsReturnsError = true; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "inbound error response send must reuse the same invoke context instance seen in the handler"; + } + + void AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Fail()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error, 250); + EXPECT_EQ(ROSE_TE_TIMEOUT, roseResult) << "client must time out when the inbound response never arrives; ROSE_TE_TRANSPORTFAILED applies only to a failed send on the caller side"; + + EXPECT_TRUE(ServerInboundObservation().HandlerWasInvoked()); + EXPECT_GT(ServerInboundObservation().TransportSendCount(), 0u); + EXPECT_TRUE(ServerInboundObservation().SameInstanceHandlerToTransport()) << "failed inbound response send must still pass the handler invoke context to transport"; + } + + void AssertUnknownInboundOperationRejectUsesCustomInvokeContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + const long roseResult = m_clientSettingsModule.InvokeUnknownOperation(); + EXPECT_EQ(ROSE_REJECT_UNKNOWNOPERATION, roseResult); + EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()) << "module handler must not run for unknown operations"; + + const auto& transportSnapshot = ServerInboundObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "inbound reject must still use CreateInvokeContext() product"; + EXPECT_EQ("server-session", transportSnapshot.LocalSessionId()); + } + + void AssertUnparsableInboundDoesNotReachHandler(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.ReceiveRaw(MalformedPayload(encoding)); + + EXPECT_FALSE(ServerInboundObservation().HandlerWasInvoked()); + auto count = ServerInboundObservation().TransportSendCount(); + EXPECT_EQ(0u, count); + } + + void AssertOutboundCallerContextReachesTransport(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("outbound-request"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()); + EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); + EXPECT_EQ("outbound-request", transportSnapshot.TelemetryNote()); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + } + + void AssertOutboundStubCreatedContextReachesTransport(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + + const long roseResult = m_clientSettingsModule.InvokeGetSettings(&argument, &result, &error); + EXPECT_EQ(ROSE_NOERROR, roseResult); + + const auto& transportSnapshot = ClientOutboundTransportObservation().LastTransportSnapshot(); + ASSERT_TRUE(transportSnapshot.WasCaptured()); + EXPECT_TRUE(transportSnapshot.IsSessionContext()) << "outbound invoke without caller context must use CreateInvokeContext() product"; + EXPECT_EQ("client-session", transportSnapshot.LocalSessionId()); + EXPECT_EQ("client-session", transportSnapshot.InvokeSessionId()); + } + + void AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("caller-owned"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext); + EXPECT_EQ(ROSE_NOERROR, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("caller-owned", pContext->TelemetryNote()); + + const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::REMOTE_RESULT); + ASSERT_NE(nullptr, pTelemetry); + ASSERT_TRUE(pTelemetry->m_pctx); + const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); + ASSERT_NE(nullptr, pSessionTelemetryContext); + EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); + EXPECT_EQ("prepared:caller-owned", pSessionTelemetryContext->TelemetryNote()); + } + + void AssertOutboundEncodeFailureStillUsesCallerContext(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnSetSettingsArgument argument; + SetSettingsValues(argument.settings, true, std::string(10'000'000, 'x')); + AsnSetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnSetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnSetSettings"); + pContext->SetTelemetryNote("encode-failure"); + + const long roseResult = m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnSetSettings", 250, pContext); + EXPECT_EQ(ROSE_TE_ENCODE_FAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("encode-failure", pContext->TelemetryNote()); + EXPECT_EQ(0u, ClientOutboundTransportObservation().TransportSendCount()) << "encode failure must occur before transport send"; + } + + void AssertNoTransportReturnsTransportFailedForInvoke(const TransportEncoding encoding) + { + ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; + endpoint.SetEncoding(encoding); + SettingsServiceModule serviceModule(endpoint); + ENetUC_Settings_ManagerROSE component(&endpoint); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(endpoint.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = endpoint.CreateSessionInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("no-transport"); + + const long roseResult = component.Invoke_asnGetSettings(&argument, &result, &error, 250, pContext); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("no-transport", pContext->TelemetryNote()); + } + + void AssertNoTransportReturnsTransportFailedForEvent(const TransportEncoding encoding) + { + ObservedRuntimeEndpoint endpoint{L"NoTransportEndpoint", "solo-session"}; + endpoint.SetEncoding(encoding); + SettingsServiceModule serviceModule(endpoint); + ENetUC_Settings_ManagerROSE component(&endpoint); + + AsnSettingsChangedArgument eventArgument; + SetSettingsValues(eventArgument.settings, true, "event-without-transport"); + auto pContext = endpoint.CreateSessionInvokeContext(nullptr, "asnSettingsChanged"); + pContext->SetTelemetryNote("event-no-transport"); + + const long roseResult = component.Event_asnSettingsChanged(&eventArgument, pContext); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("event-no-transport", pContext->TelemetryNote()); + } + + void AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_client.Transport().EnqueueAction(TransportAction::Fail()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("transport-failure"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_TE_TRANSPORTFAILED, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + } + + void AssertOutboundTimeoutKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Drop()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("timeout"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 25); + EXPECT_EQ(ROSE_TE_TIMEOUT, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + + const SnaccTelemetryData* pTelemetry = FindOutboundWaitTelemetry(TelemetryEntries(), SnaccTelemetryData::Reason::TIMEOUT); + ASSERT_NE(nullptr, pTelemetry); + ASSERT_TRUE(pTelemetry->m_pctx); + const auto* pSessionTelemetryContext = AsSessionContext(pTelemetry->m_pctx.get()); + ASSERT_NE(nullptr, pSessionTelemetryContext); + EXPECT_TRUE(pSessionTelemetryContext->WasPreparedForTelemetry()); + EXPECT_EQ("prepared:timeout", pSessionTelemetryContext->TelemetryNote()); + } + + void AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.implementGetSettings = false; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("remote-reject"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_REJECT_FUNCTIONMISSING, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("remote-reject", pContext->TelemetryNote()); + } + + void AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + HandlerModes handlerModes; + handlerModes.getSettingsReturnsError = true; + ConfigureServerHandlers(handlerModes); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("remote-error"); + + const long roseResult = m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 250); + EXPECT_EQ(ROSE_ERROR_VALUE, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("remote-error", pContext->TelemetryNote()); + } + + void AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_client.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("decode-failure"); + + auto responseFuture = std::async(std::launch::async, [&]() { return m_client.SendInvoke(invokeMsg.GetPtr(), &result, &error, "asnGetSettings", 250, pContext); }); + + for (int i = 0; i < 20 && m_client.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1U, m_client.Transport().QueuedMessageCount()); + m_client.Transport().TakeQueuedMessages(); + + std::string responsePayload; + AsnBool wrongPayload(true); + ASSERT_EQ(ROSE_NOERROR, m_server.EncodeResult(invokeMsg.GetPtr()->invokeID, &wrongPayload, responsePayload)); + m_client.ReceiveRaw(responsePayload); + + const long roseResult = responseFuture.get(); + EXPECT_EQ(ROSE_RE_DECODE_FAILED, roseResult); + ASSERT_GT(ClientOutboundTransportObservation().TransportSendCount(), 0u); + EXPECT_EQ(pContext.get(), ClientOutboundTransportObservation().LastContextAddress()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("decode-failure", pContext->TelemetryNote()); + } + + void AssertOrphanResultDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-invoke"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + AsnBool orphanPayload(true); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-invoke", pContext->TelemetryNote()); + } + + void AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + + AsnBool orphanPayload(true); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeResult(4242, &orphanPayload, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + EXPECT_TRUE(HasInboundResponseTelemetryForOperation(TelemetryEntries(), 0)); + } + + void AssertOrphanErrorDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-error"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + AsnRequestError orphanError; + orphanError.iErrorDetail = 4242; + orphanError.u8sErrorString.setASCII("orphan error"); + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeError(5252, &orphanError, orphanResponse)); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-error", pContext->TelemetryNote()); + } + + void AssertOrphanRejectDoesNotBreakPendingInvoke(const TransportEncoding encoding) + { + InitializeEndpoints(encoding); + m_server.Transport().EnqueueAction(TransportAction::Queue()); + + AsnGetSettingsArgument argument; + AsnGetSettingsResult result; + AsnRequestError error; + SnaccScopedInvokeMessage invokeMsg(m_client.GetNextInvokeID(), OPID_asnGetSettings, &argument); + auto pContext = CreateClientInvokeContext(invokeMsg.GetPtr(), "asnGetSettings"); + pContext->SetTelemetryNote("pending-reject"); + + auto pendingInvoke = std::async(std::launch::async, [&]() { return m_clientSettingsModule.InvokeGetSettingsWithContext(&argument, &result, &error, pContext, 500); }); + + for (int i = 0; i < 20 && m_server.Transport().QueuedMessageCount() == 0; ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + ASSERT_EQ(1u, m_server.Transport().QueuedMessageCount()); + + std::string orphanResponse; + ASSERT_EQ(ROSE_NOERROR, m_client.EncodeRejectInvoke(6262, InvokeProblem::unrecognisedOperation, orphanResponse, "orphan reject")); + m_client.ReceiveRaw(orphanResponse); + + FlushServerOutbound(); + EXPECT_EQ(ROSE_NOERROR, pendingInvoke.get()); + EXPECT_FALSE(pContext->WasPreparedForTelemetry()); + EXPECT_EQ("pending-reject", pContext->TelemetryNote()); + } + }; + + TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextBer) + { + AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, InboundHandlerReceivesCustomInvokeContextJson) + { + AssertInboundHandlerReceivesCustomInvokeContext(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextBer) + { + AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, InboundResponseSendReusesHandlerInvokeContextJson) + { + AssertInboundResponseSendReusesHandlerInvokeContext(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextBer) + { + AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, InboundHandlerRejectReusesInvokeContextJson) + { + AssertInboundHandlerRejectReusesInvokeContext(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextBer) + { + AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, InboundApplicationErrorReusesInvokeContextJson) + { + AssertInboundApplicationErrorReusesInvokeContext(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextBer) + { + AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, InboundResponseSendFailureKeepsInvokeContextJson) + { + AssertInboundResponseSendFailureKeepsInvokeContextOnAttemptedSend(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextBer) + { + AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, UnknownInboundOperationRejectUsesCustomInvokeContextJson) + { + AssertUnknownInboundOperationRejectUsesCustomInvokeContext(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerBer) + { + AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, UnparsableInboundDoesNotReachHandlerJson) + { + AssertUnparsableInboundDoesNotReachHandler(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportBer) + { + AssertOutboundCallerContextReachesTransport(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextReachesTransportJson) + { + AssertOutboundCallerContextReachesTransport(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportBer) + { + AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundStubCreatedContextReachesTransportJson) + { + AssertOutboundStubCreatedContextReachesTransport(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsBer) + { + AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundCallerContextSurvivesUntilCallerScopeEndsJson) + { + AssertOutboundCallerContextSurvivesUntilCallerScopeEnds(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundEncodeFailureKeepsCallerContextJson) + { + AssertOutboundEncodeFailureStillUsesCallerContext(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeBer) + { + AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForInvokeJson) + { + AssertNoTransportReturnsTransportFailedForInvoke(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventBer) + { + AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, NoTransportReturnsTransportFailedForEventJson) + { + AssertNoTransportReturnsTransportFailedForEvent(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextBer) + { + AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundTransportFailureKeepsCallerContextJson) + { + AssertOutboundTransportFailureKeepsCallerContextOnSendAttempt(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextBer) + { + AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundTimeoutKeepsCallerContextJson) + { + AssertOutboundTimeoutKeepsCallerContextOnRequestSend(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextBer) + { + AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundRemoteRejectKeepsCallerContextJson) + { + AssertOutboundRemoteRejectKeepsCallerContextOnRequestSend(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextBer) + { + AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundRemoteErrorKeepsCallerContextJson) + { + AssertOutboundRemoteErrorKeepsCallerContextOnRequestSend(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextBer) + { + AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OutboundMalformedResponseKeepsCallerContextJson) + { + AssertOutboundMalformedResponseKeepsCallerContextThroughDecodeFailure(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeBer) + { + AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OrphanResultDoesNotBreakPendingInvokeJson) + { + AssertOrphanResultDoesNotBreakPendingInvoke(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryBer) + { + AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OrphanResultEmitsInboundResponseTelemetryJson) + { + AssertOrphanResultEmitsInboundResponseTelemetryWithoutPendingOperation(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeBer) + { + AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OrphanErrorDoesNotBreakPendingInvokeJson) + { + AssertOrphanErrorDoesNotBreakPendingInvoke(TransportEncoding::JSON); + } + + TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeBer) + { + AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::BER); + } + + TEST_F(InvokeContextRuntimeTest, OrphanRejectDoesNotBreakPendingInvokeJson) + { + AssertOrphanRejectDoesNotBreakPendingInvoke(TransportEncoding::JSON); + } + +} // namespace sample_runtime_tests diff --git a/cpp-lib/tests/runtime_correctness_notes.md b/cpp-lib/tests/runtime_correctness_notes.md index 0630b1b..39a9414 100644 --- a/cpp-lib/tests/runtime_correctness_notes.md +++ b/cpp-lib/tests/runtime_correctness_notes.md @@ -6,7 +6,7 @@ entry_for: - C++ runtime behavior - runtime correctness tests - ROSE telemetry and shutdown semantics -purpose: Record intended C++ runtime semantics before tests or implementation are tightened. +purpose: Record intended C++ runtime semantics and implementation status for ROSE correctness areas exercised by cpp-lib runtime tests. read_when: - Changing cpp-lib runtime behavior, telemetry, shutdown, or decode-error handling - Adding or reviewing runtime correctness tests @@ -18,9 +18,9 @@ related_docs: # Runtime Correctness Notes This note records the intended semantics for selected `cpp-lib` runtime behaviors -before tightening tests or changing the implementation. The goal is to align the -runtime to the public API contract and to common operator expectations rather -than simply preserving whatever behavior exists today. +and whether the current tree implements them. The goal is to align the runtime +to the public API contract and to common operator expectations rather than +simply preserving whatever behavior exists today. Primary reference points: - `cpp-lib/include/SnaccROSEBase.h` @@ -30,15 +30,20 @@ Primary reference points: ## Summary -| Area | Current Behavior | Intended Behavior | -| --- | --- | --- | -| `StopProcessing()` | Completes pending operations, but does not block new work in practice | Shutdown must refuse new outbound work and stop inbound invoke/event dispatch | -| `iTimeout == 0` telemetry | Reported as `Outcome::UNHANDLED` with `WAIT_SKIPPED` | Treated as a successful fire-and-forget dispatch, not as a failure | -| Response payload decode after a valid reply envelope | Caller sees decode failure, telemetry still looks like remote result/error | Telemetry must primarily reflect the caller-visible outcome | -| `OnBinaryDataBlockResult()` decode failures | Logs and telemetry are emitted, but `OnRoseDecodeError()` parity is missing | Result-path decode failure handling should mirror `OnBinaryDataBlock()` | +| Area | Status | Semantics | Primary tests | +| --- | --- | --- | --- | +| `StopProcessing()` shutdown gate | Implemented | Refuse new outbound work; block inbound handler dispatch; complete pending ops with `ROSE_TE_SHUTDOWN` | `PublicApiRuntimeTest.StopProcessingBlocks*`, `LifecycleRuntimeTest.StopProcessing*` | +| Fire-and-forget (`iTimeout == 0`) telemetry | Implemented | `Outcome::DISPATCHED` + `Reason::WAIT_SKIPPED`, not `UNHANDLED` | `TelemetryRuntimeTest.WaitSkippedTelemetry*` | +| Response payload decode telemetry | Implemented | Caller-visible `ROSE_RE_DECODE_FAILED` drives `UNHANDLED` + `DECODE_FAILED`, not envelope kind | `TelemetryRuntimeTest.*PayloadDecodeFailureTelemetry*` | +| Inbound decode failures and ROSE rejects | Implemented | Garbage wire silent; targeted reject only after envelope decode | `InvokeContextRuntimeTest.UnparsableInbound*`, section 5 | +| `OnBinaryDataBlockResult()` decode-error hooks | Implemented | `OnRoseDecodeError()` and `bAlreadyTransportLogged` parity with `OnBinaryDataBlock()` | `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHook*` | +| Inbound `ROSEMessage` ownership | Implemented | `unique_ptr` at decode sites; `std::move` through dispatch | Section 6; `InvokeContextRuntimeTest` suite | +| Outbound encode / `Send()` ownership | Implemented | RAII encode helpers detach borrowed arms on scope exit (including encode exceptions) | Section 7; outbound encode-failure tests in `InvokeContextRuntimeTest` | ## 1. `StopProcessing()` Shutdown Contract +### Status: implemented + ### Public contract `SnaccROSEBase` documents shutdown as a hard stop: @@ -51,13 +56,25 @@ Primary reference points: void StopProcessing(bool bStop = true); ``` -### Current behavior +### Intended behavior + +Treat `StopProcessing(true)` as a real runtime shutdown gate: + +1. New outbound invokes and events must fail fast with `ROSE_TE_SHUTDOWN`. +2. Pending operations must still be completed with `ROSE_TE_SHUTDOWN`. +3. New inbound invokes and inbound events must not be dispatched to application + handlers while shutdown is active. +4. Late inbound responses that arrive after pending operations were force- + completed may be ignored, but they must not resurrect completed work. +5. `StopProcessing(false)` re-enables processing; callers must treat that as an + explicit restart, not an incidental side effect. + +### Implementation -The implementation only toggles `m_bProcessingAllowed` and completes pending -operations. In the current tree, the flag is not enforced by the send or receive -paths. +`StopProcessing(true)` clears `m_bProcessingAllowed` and completes all pending +operations with `ROSE_TE_SHUTDOWN`: -```330:338:cpp-lib/src/SnaccROSEBase.cpp +```605:628:cpp-lib/src/SnaccROSEBase.cpp void SnaccROSEBase::StopProcessing(bool bStop /*= true*/) { { @@ -65,306 +82,313 @@ void SnaccROSEBase::StopProcessing(bool bStop /*= true*/) m_bProcessingAllowed = bStop ? false : true; } - CompleteAllPendingOperations(); + if (bStop) + CompleteAllPendingOperations(); } +... + for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++) + it->second->CompleteOperation(ROSE_TE_SHUTDOWN); ``` -```346:352:cpp-lib/src/SnaccROSEBase.cpp -void SnaccROSEBase::CompleteAllPendingOperations() -{ - std::lock_guard guard(m_InternalProtectMutex); +Outbound choke points check `IsProcessingAllowed()` before creating pending +operations or sending: - for (auto it = m_PendingOperations.begin(); it != m_PendingOperations.end(); it++) - it->second->CompleteOperation(ROSE_TE_SHUTDOWN, NULL); -} +```1604:1610:cpp-lib/src/SnaccROSEBase.cpp + if (!IsProcessingAllowed()) + { + auto telemetry = SnaccTelemetryData::Create(...); + telemetry->finalize(..., SnaccTelemetryData::Reason::SHUTDOWN, ROSE_TE_SHUTDOWN, ...); + OnInvokeProcessed(telemetry); + return ROSE_TE_SHUTDOWN; + } ``` -`SendInvoke()` does not check `m_bProcessingAllowed` before creating a pending -operation, sending an invoke, or waiting for a response: - -```1500:1506:cpp-lib/src/SnaccROSEBase.cpp - auto& pendingOP = AddPendingOperation(pinvoke->invokeID, pinvoke->operationID, szResolvedOperationName); +`SendEvent()` uses the same gate and returns `ROSE_TE_SHUTDOWN` without sending. - size_t stRequestData = 0; - long lRoseResult = Send(pinvoke, szOperationName, ctx, &stRequestData); - pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pinvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); +Inbound invoke/event dispatch is blocked in `OnInvokeMessage()`: - if (lRoseResult == 0) +```1324:1325:cpp-lib/src/SnaccROSEBase.cpp + if (!IsProcessingAllowed()) + lResult = ROSE_TE_SHUTDOWN; ``` -### Why this is inconsistent +Wire data may still be decoded on the receive path; handlers are not reached +while shutdown is active. + +### Tests that enforce this -- The public header promises that new calls are blocked after shutdown. -- The implementation only completes already pending work. -- Callers and operators cannot rely on shutdown to be a real safety boundary. +- `PublicApiRuntimeTest.StopProcessingBlocksNewOutboundInvokesAndEvents` +- `PublicApiRuntimeTest.StopProcessingBlocksInboundDispatchUntilReEnabled` +- `LifecycleRuntimeTest.StopProcessingCompletesPendingInvokeWithShutdownBer` +- `LifecycleRuntimeTest.StopProcessingCompletesPendingInvokeWithShutdownJson` +- `LifecycleRuntimeTest.PendingInvokeCanRecoverAfterShutdownOnNextFixtureSetupBer` +- `LifecycleRuntimeTest.PendingInvokeCanRecoverAfterShutdownOnNextFixtureSetupJson` + +## 2. Fire-And-Forget Invoke Telemetry (`iTimeout == 0`) + +### Status: implemented ### Intended behavior -Treat `StopProcessing(true)` as a real runtime shutdown gate: +Fire-and-forget is a successful local dispatch of an invoke whose remote outcome +is intentionally unknown to this runtime instance: -1. New outbound invokes and events must fail fast with `ROSE_TE_SHUTDOWN`. -2. Pending operations must still be completed with `ROSE_TE_SHUTDOWN`. -3. New inbound invokes and inbound events must not be dispatched to application - handlers while shutdown is active. -4. Late inbound responses that arrive after pending operations were force- - completed may be ignored, but they must not resurrect completed work. -5. `StopProcessing(false)` may re-enable the runtime if that is part of the - supported API, but the behavior should be explicit and documented as a - restart of processing, not an incidental side effect. - -### Tests that should enforce this later - -- `SendInvoke()` after shutdown returns `ROSE_TE_SHUTDOWN` without creating a - new pending operation. -- `SendEvent()` after shutdown returns `ROSE_TE_SHUTDOWN`. -- An inbound invoke fed through `OnBinaryDataBlock()` during shutdown does not - reach application handlers. -- Existing pending operations complete with `ROSE_TE_SHUTDOWN`. -- If restart remains supported, `StopProcessing(false)` has one explicit test - proving the intended resumed behavior. - -### Likely code paths to change - -- `SnaccROSEBase::SendInvoke()` -- `SnaccROSEBase::SendEvent()` -- potentially `SnaccROSEBase::Send()` as the shared outbound choke point -- `SnaccROSEBase::OnBinaryDataBlock()` -- `SnaccROSEBase::OnBinaryDataBlockResult()` -- possibly `SnaccROSEBase::OnROSEMessage()` for central inbound gating +1. `Outcome::DISPATCHED` for "sent, not awaited". +2. `Reason::WAIT_SKIPPED` preserves the explicit cause. +3. `Stage::OUTBOUND_WAIT` is acceptable for now; a finer stage taxonomy is + deferred until async invokes that complete via callback reshape outbound + lifecycle telemetry anyway. -## 2. Fire-And-Forget Invoke Telemetry (`iTimeout == 0`) +`WAIT_SKIPPED` must not be classified under the same top-level failure bucket as +transport errors, timeouts, shutdown, invalid responses, or decode failures. -### Current behavior +### Implementation -When the caller explicitly chooses not to wait, `SendInvoke()` treats the call -as locally successful and stores `ROSE_NOERROR`: +When `iTimeout == 0`, `SendInvoke()` records local success (`ROSE_NOERROR`) and +does not wait for a response. `FinalizeTelemetry()` then classifies the +lifecycle as dispatched, not unhandled: -```1529:1533:cpp-lib/src/SnaccROSEBase.cpp - else - { - lRoseResult = ROSE_NOERROR; - pendingOP.m_lRoseResult = lRoseResult; - } +```581:581:cpp-lib/src/SnaccROSEBase.cpp + m_pTelemetry->finalize(SnaccTelemetryData::Outcome::DISPATCHED, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::WAIT_SKIPPED, m_lRoseResult, std::nullopt, std::move(pctx)); ``` -Later, telemetry finalization sees "no answer message" plus `ROSE_NOERROR` and -records the lifecycle as `Outcome::UNHANDLED` with `Reason::WAIT_SKIPPED`. +`SnaccTelemetryData::Outcome::DISPATCHED` and its debug text are defined in +`cpp-lib/include/SnaccTelemetry.h` and `cpp-lib/src/SnaccTelemetry.cpp`. + +### Tests that enforce this + +- `TelemetryRuntimeTest.WaitSkippedTelemetryBer` +- `TelemetryRuntimeTest.WaitSkippedTelemetryJson` -```299:306:cpp-lib/src/SnaccROSEBase.cpp - if (m_lRoseResult != ROSE_NOERROR) +## 3. Outbound Telemetry After Response Payload Decode Failure + +### Status: implemented + +### Intended behavior + +For outbound invoke telemetry, the final caller-visible result is the +authoritative classification: + +1. If the response envelope was received but payload decode fails, telemetry + finalizes as `Outcome::UNHANDLED`. +2. The reason is `DECODE_FAILED`. +3. The result code is `ROSE_RE_DECODE_FAILED`. +4. Envelope kind (`result` vs `error`) must not override the primary outcome. + +### Implementation + +`HandleInvokeResult()` can return `ROSE_RE_DECODE_FAILED` after a valid envelope +when the embedded result or error payload cannot be decoded. `FinalizeTelemetry()` +compares the stored pending-op result with the final caller-visible result and +prefers the final outcome when they differ: + +```549:552:cpp-lib/src/SnaccROSEBase.cpp + if (m_pAnswerMessage && lFinalRoseResult != m_lRoseResult) { - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, GetOutboundUnhandledStageFromResult(m_lRoseResult), GetUnhandledReasonFromResult(m_lRoseResult), m_lRoseResult, std::nullopt, std::move(pctx)); + m_pTelemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, GetOutboundUnhandledStageFromResult(lFinalRoseResult), GetUnhandledReasonFromResult(lFinalRoseResult), lFinalRoseResult, m_stResponseData, std::move(pctx)); return; } - - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::WAIT_SKIPPED, m_lRoseResult, std::nullopt, std::move(pctx)); ``` -### Why this is inconsistent +When payload decode succeeds, envelope kind still drives `RESULT`, `ERR`, or +`REJECT` telemetry as before. -- `WAIT_SKIPPED` is intentional caller behavior, not a failure condition. -- `Outcome::UNHANDLED` is otherwise used for abnormal paths such as transport, - timeout, shutdown, invalid response, and decode failure. -- A caller intentionally using fire-and-forget currently looks like a failure in - telemetry aggregations. +### Tests that enforce this -### Intended behavior +- `TelemetryRuntimeTest.ResultPayloadDecodeFailureTelemetryBer` +- `TelemetryRuntimeTest.ResultPayloadDecodeFailureTelemetryJson` +- `TelemetryRuntimeTest.ErrorPayloadDecodeFailureTelemetryBer` +- `TelemetryRuntimeTest.ErrorPayloadDecodeFailureTelemetryJson` -Fire-and-forget should be treated as a successful local dispatch of an invoke -whose remote outcome is intentionally unknown to this runtime instance. +## 4. `OnBinaryDataBlockResult()` Decode-Error Hook and Logging Parity -Preferred model: +### Status: implemented -1. Introduce a dedicated successful outcome such as `DISPATCHED` or `PENDING` - for "sent, not awaited". -2. Keep `Reason::WAIT_SKIPPED` to preserve the explicit cause. -3. Keep `Stage::OUTBOUND_WAIT` or rename it later if a more precise stage model - is introduced. +Both inbound entry points call `OnRoseDecodeError()` for comparable decode +failure classes (BER envelope decode, JSON envelope decode, JSON parse failure, +unknown encoding). Both pass the real `bAlreadyTransportLogged` value derived +from `LogTransportData()` return value before invoking the hook. -If the enum surface cannot be changed immediately, the minimum semantic rule -should still be: +Shared private methods on `SnaccROSEBase` centralize logging, hook invocation, +optional reject, and telemetry: -- do not classify `WAIT_SKIPPED` under the same top-level failure bucket used - for true unhandled faults. +| Method | Role | +| --- | --- | +| `HandleInboundEnvelopeSnaccDecodeFailure` | `SnaccException` after BER `BDec` or JSON `JDec` | +| `HandleInboundJsonParseDecodeFailure` | `SJson::Reader::parse` failure | +| `HandleInboundUnknownEncodingDecodeFailure` | Unknown `m_eTransportEncoding` | +| `HandleInboundOuterDecodeFailure` | Outer `catch` around the encoding switch | +| `EmitInboundDecodeFailureTelemetry` | `OnInvokeProcessed` for decode failures | -### Tests that should enforce this later +`OnBinaryDataBlock()` passes `bSendReject=true` into the envelope helper; +`OnBinaryDataBlockResult()` passes `bSendReject=false`. -- `SendInvoke(..., iTimeout = 0)` produces a non-failure telemetry outcome. -- The telemetry reason remains `WAIT_SKIPPED`. -- The runtime reports local send success and does not fabricate a remote result. +### Tests that enforce this -### Likely code paths to change +- `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookBer` +- `PublicApiSmokeTest.OnBinaryDataBlockResultDecodeErrorsInvokeHookJson` -- `SnaccROSEPendingOperation::FinalizeTelemetry()` -- `SnaccTelemetryData::Outcome` in `cpp-lib/include/SnaccTelemetry.h` -- any debug-text helpers in `cpp-lib/src/SnaccTelemetry.cpp` +## 5. Inbound Decode Layers, Reject Policy, and BER vs JSON -## 3. Outbound Telemetry After Response Payload Decode Failure +### Why BER and JSON are not symmetric at the wire layer -### Current behavior +**BER** is decoded incrementally as nested TLVs. The runtime can fail at +different depths on the same buffer: -`HandleInvokeResult()` can turn a reply that had a valid envelope into a final -caller-visible decode failure when the embedded result or error payload cannot -be decoded. +1. **Wire garbage** — not even a decodable `ROSEMessage` (for example truncated + tag/length). +2. **Envelope incomplete** — some bytes consumed, but `ROSEMessage::BDec` did not + finish; generated CHOICE `choiceId` must not be trusted (codegen defers + `choiceId` until the selected arm decodes successfully). +3. **Envelope OK, payload bad** — invoke envelope is valid; operation argument + decode fails later in `OnInvokeMessage`. -```1745:1747:cpp-lib/src/SnaccROSEBase.cpp - lRoseResult = DecodeResponse(pResponseMsg, &pResult, &pError, ctx); +**JSON** does not offer an envelope-only parse for invalid wire text. +`SJson::Reader::parse` is all-or-nothing on the payload after the `J` length +prefix: - if (lRoseResult == ROSE_NOERROR) -``` +- If parse fails, there is no `SJson::Value` tree and no partial ROSE structure + to inspect. +- Wire failure and “not a ROSE JSON object” collapse into one step for malformed + syntax. +- Only after parse succeeds does `ROSEMessage::JDec` run field-by-field (layer 2 + above). -```1787:1796:cpp-lib/src/SnaccROSEBase.cpp - catch (const SnaccException& ex) - { - SJson::Value err; - err["exception"] = ex.what(); - err["method"] = __FUNCTION__; - err["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(err); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); - - lRoseResult = ROSE_RE_DECODE_FAILED; - } -``` +So BER admits **layered** failure classification at runtime; JSON only admits +layers **after** syntactically valid JSON exists. -But outbound telemetry finalization uses the received envelope kind stored in -`m_pAnswerMessage`, not the final caller-visible result returned after payload -decode: +### Intended reject policy (implemented) -```280:292:cpp-lib/src/SnaccROSEBase.cpp - if (m_pAnswerMessage) - { - switch (m_pAnswerMessage->choiceId) - { - case ROSEMessage::resultCid: - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::RESULT, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::REMOTE_RESULT, m_lRoseResult, m_stResponseData, pctx); - break; - case ROSEMessage::errorCid: - m_pTelemetry->finalize(SnaccTelemetryData::Outcome::ERR, SnaccTelemetryData::Stage::OUTBOUND_WAIT, SnaccTelemetryData::Reason::REMOTE_ERROR, m_lRoseResult, m_stResponseData, pctx); - break; -``` +Outbound ROSE rejects must be **correlatable and semantically honest**. Do not +claim `mistypedArgument` when no invoke was successfully decoded. -### Why this is inconsistent +| Layer | What failed | `bRoseEnvelopeDecoded` | Outbound ROSE reject? | +| --- | --- | --- | --- | +| Wire / syntax | BER garbage, JSON `parse` fail, unknown encoding | n/a (no envelope) | **No** — log, `OnRoseDecodeError`, telemetry only | +| Envelope | `BDec` / `JDec` on `ROSEMessage` did not complete | `false` | **No** | +| Envelope OK, invoke path | Decode or dispatch failed after envelope succeeded | `true` and `invoke` present | **Yes** on `OnBinaryDataBlock()` — `mistypedArgument` with real `invokeID` | +| Argument | Operation argument decode in handler path | n/a (handler stage) | **Yes** — `OnInvokeMessage` / handler reject path | -- The public caller gets a decode failure. -- Telemetry still says the operation completed as a clean remote result or - remote error. -- This makes telemetry disagree with the API result that application code sees. +Garbage wire therefore gets **no response** on the application ROSE layer (common +RPC practice: the caller times out; an uncorrelated `invokednull` reject does not +help a pending client invoke). -### Intended behavior +### Intentional asymmetry: `OnBinaryDataBlock()` vs `OnBinaryDataBlockResult()` -For outbound invoke telemetry, the final caller-visible result must be the -authoritative classification. +| Entry point | Role | Reject on decode failure? | +| --- | --- | --- | +| `OnBinaryDataBlock()` | Inbound invokes/events (server receive path) | **May** send targeted `mistypedArgument` when envelope decode succeeded and `invoke` is known | +| `OnBinaryDataBlockResult()` | Inbound results/errors/rejects (client response path) | **Must not** send rejects for decode failures; log + hook + telemetry only | -That means: +Hook and logging parity between the two paths is required. **Reject parity is not** +— the response path must not fabricate server-side rejects when a reply cannot be +decoded. -1. If the response envelope was received but payload decode fails, telemetry - should finalize as `Outcome::UNHANDLED`. -2. The reason should be `DECODE_FAILED`. -3. The result code should be `ROSE_RE_DECODE_FAILED`. -4. If later needed, the runtime may keep a secondary field for "remote envelope - kind received", but that must not override the primary outcome. +Legacy reject branches were removed from `OnBinaryDataBlockResult()` decode +catches; that path is telemetry-only on decode failure. -### Tests that should enforce this later +### Tests that enforce this -- A malformed result payload yields `ROSE_RE_DECODE_FAILED` to the caller and - telemetry `UNHANDLED + DECODE_FAILED`. -- A malformed error payload yields the same shape. -- The telemetry record must no longer be `REMOTE_RESULT` or `REMOTE_ERROR` in - those cases. +- `InvokeContextRuntimeTest.UnparsableInboundDoesNotReachHandlerBer` +- `InvokeContextRuntimeTest.UnparsableInboundDoesNotReachHandlerJson` +- CHOICE `choiceId` deferral: `compiler/back-ends/c++-gen/gen-code.c` (regenerated + `SNACCROSE.cpp`) -### Likely code paths to change +### Likely code paths -- `SnaccROSEBase::HandleInvokeResult()` -- `SnaccROSEPendingOperation::FinalizeTelemetry()` -- potentially `SnaccROSEPendingOperation::CompleteOperation()` if more state is - needed than envelope kind plus raw result code +- `SnaccROSEBase::OnBinaryDataBlock()` +- `SnaccROSEBase::OnBinaryDataBlockResult()` +- `SnaccROSEBase::OnInvokeMessage()` (argument-layer rejects) +- `compiler/back-ends/c++-gen/gen-code.c` (CHOICE decode / `choiceId`) -## 4. `OnBinaryDataBlockResult()` Decode-Error Hook and Logging Parity +## 6. `ROSEMessage` Ownership on Inbound Decode Paths -### Current behavior +### Status: implemented -`OnBinaryDataBlock()` invokes `OnRoseDecodeError()` on BER, JSON decode, JSON -parse, and unknown-encoding failures. It also updates `bLogTransportData` with -the actual return value from `LogTransportData()` before calling the hook. +### Contract -```780:792:cpp-lib/src/SnaccROSEBase.cpp - catch (const SnaccException& ex) - { - if (bLogTransportData) - bLogTransportData = LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, ulSize, nullptr, nullptr); +Inbound decode paths allocate with `std::make_unique()`. After a +successful envelope decode (`BDec` / `JDec`), reject-relevant invoke fields are +snapshotted into `InboundInvokeRejectContext` (invoke ID, operation ID, +operation name). Ownership then moves into `OnROSEMessage()` via `std::move`. +If dispatch throws, the outer `catch` uses the snapshot for targeted +`mistypedArgument` rejects — not the moved-away message. - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); +`OnROSEMessage()` takes `std::unique_ptr`: - OnRoseDecodeError(bLogTransportData, SNACC::TransportEncoding::BER, lpBytes, ulSize, ex.what()); -``` +| Stage | Owner | +| --- | --- | +| Before envelope decode | Local `unique_ptr` in the decode `try` block | +| After envelope decode, before `OnROSEMessage` | Local `unique_ptr` + optional `InboundInvokeRejectContext` snapshot | +| Invoke/event dispatch | `OnInvokeMessage(std::unique_ptr)` — destroyed after dispatch | +| Matched result/error/reject | `CompletePendingOperation(std::move)` → `m_pAnswerMessage` | +| Orphan result/error/reject | `CompletePendingOperation()` when lookup fails | +| `SnaccException` before envelope decode | Local `unique_ptr` destroyed on scope exit | +| `SnaccException` after envelope decode | `rejectCtx` snapshot drives reject/telemetry; `unique_ptr` destroyed on scope exit | -`OnBinaryDataBlockResult()` performs similar logging and telemetry work, but it -never calls `OnRoseDecodeError()` in the corresponding failure cases. - -```493:503:cpp-lib/src/SnaccROSEBase.cpp - catch (const SnaccException& ex) - { - if (bLogTransportData) - LogTransportData(false, m_eTransportEncoding, nullptr, lpBytes, lSize, nullptr, nullptr); - - SJson::Value error; - error["exception"] = ex.what(); - error["method"] = __FUNCTION__; - error["error"] = (int)ex.m_errorCode; - std::string strError = getPrettyPrinted(error); - PrintJSONToLog(false, true, nullptr, strError.c_str(), strError.length()); -``` +Reject policy in decode `catch` blocks: -### Why this is inconsistent +| Entry point | Send `mistypedArgument` reject? | +| --- | --- | +| `OnBinaryDataBlock()` | Yes, when `rejectCtx` is present and invoke ID ≠ 99999 | +| `OnBinaryDataBlockResult()` | No — telemetry only | -- `OnRoseDecodeError()` is the dedicated extension point for decode failures. -- Result-path decode failures are still decode failures, but the override hook is - bypassed there. -- Consumers overriding that hook cannot get symmetric behavior for the two - inbound entry points. -- The `bAlreadyTransportLogged` signal is only maintained on the - `OnBinaryDataBlock()` side. +### Likely code paths -### Intended behavior +- `SnaccROSEBase::OnBinaryDataBlock()` +- `SnaccROSEBase::OnBinaryDataBlockResult()` +- `SnaccROSEBase::OnROSEMessage()` +- `SnaccROSEBase::CompletePendingOperation()` +- `SnaccROSEPendingOperation::CompleteOperation()` -`OnBinaryDataBlockResult()` should mirror `OnBinaryDataBlock()` for decode -failure handling unless there is an explicitly documented reason not to. +## 7. Outbound Encode and `Send()` Ownership -That means: +### Status: implemented -1. invoke `OnRoseDecodeError()` for comparable decode-failure classes -2. pass the real `bAlreadyTransportLogged` state derived from - `LogTransportData()` -3. preserve any legitimate differences in reply-specific protocol handling, but - not differences in error-hook visibility +### Problem -### Tests that should enforce this later +Outbound encoding builds temporary `ROSEMessage` trees that borrow caller-owned +invoke, result, error, or reject objects. Without explicit detach before +destruction, `~ROSEMessage` can delete borrowed values. The previous pattern +used manual `// prevent delete` nulling on the happy path only, which was fragile +when encode threw. -- A test endpoint overriding `OnRoseDecodeError()` sees BER result-path decode - failures. -- The same endpoint sees JSON result-path decode failures. -- The hook receives the same "already transport logged" semantics as the normal - inbound path. +### Contract -### Likely code paths to change +1. Caller retains ownership of invoke arguments, result/error payloads, and reject + objects passed into `Send()`, `EncodeResult()`, `EncodeError()`, and + `EncodeReject()`. +2. Stack `ROSEMessage` envelopes used for encoding must detach borrowed arms in + all exit paths, including encode exceptions. +3. `ScopedInvokeOperationName` may allocate a temporary `operationName` on the + caller invoke for JSON encoding only; it removes that allocation on scope exit. -- `SnaccROSEBase::OnBinaryDataBlockResult()` -- possibly shared helper extraction between `OnBinaryDataBlock()` and - `OnBinaryDataBlockResult()` to avoid drift +### Implementation + +File-local RAII helpers in `SnaccROSEBase.cpp` (anonymous namespace): + +| Helper | Role | +| --- | --- | +| `RoseEncodeRejectBorrow` | Binds stack `ROSEReject` into `ROSEMessage` for `EncodeReject` | +| `RoseEncodeResultEnvelope` | Owns stack `ROSEResult` + encode allocations; borrows result payload | +| `RoseEncodeErrorEnvelope` | Owns stack `ROSEError` + `AsnAny` wrapper; borrows error payload | +| `ScopedEncodeInvokeBorrow` | Binds caller `ROSEInvoke` into outbound `ROSEMessage` for `Send` | +| `ScopedInvokeOperationName` | Adds/removes temporary JSON `operationName` on caller invoke | + +Each helper detaches borrowed pointers in its destructor. + +### Tests that exercise this + +- Happy-path outbound invoke/response flows across the runtime test suite +- `InvokeContextRuntimeTest.OutboundEncodeFailureKeepsCallerContextJson` (encode + failure must not corrupt caller-owned invoke context) ## Recommended Follow-Up Order -1. Fix and test the shutdown contract first, because it affects public API - guarantees and can change how the rest of the runtime is exercised. -2. Fix fire-and-forget telemetry classification so dashboards stop reporting - intentional behavior as a failure. -3. Fix response-payload decode telemetry so caller-visible failures and - telemetry agree. -4. Bring `OnBinaryDataBlockResult()` decode-error hooks into parity with - `OnBinaryDataBlock()` to avoid continuing divergence. +1. ~~Shutdown contract~~ (done) +2. ~~Fire-and-forget telemetry classification~~ (done) +3. ~~Response-payload decode telemetry~~ (done) +4. ~~Inbound decode reject policy and `OnBinaryDataBlockResult()` reject cleanup~~ (done) +5. ~~Inbound and outbound `ROSEMessage` ownership~~ (done) +6. Push branch and open PR for UCAAS-1446. +7. ~~Extract shared decode-failure helpers between `OnBinaryDataBlock()` and `OnBinaryDataBlockResult()`~~ (done) diff --git a/cpp-lib/tests/test_support/sample_runtime_harness.h b/cpp-lib/tests/test_support/sample_runtime_harness.h index 5601a1d..f8c9ec0 100644 --- a/cpp-lib/tests/test_support/sample_runtime_harness.h +++ b/cpp-lib/tests/test_support/sample_runtime_harness.h @@ -143,9 +143,10 @@ class RuntimeEndpoint; class LoopbackTransport : public ISnaccROSETransport { public: - // Binds the transport to the peer endpoint that should receive outbound data. - explicit LoopbackTransport(RuntimeEndpoint& remote) - : m_remote(remote) + // Binds the transport to the sending endpoint and its loopback peer. + LoopbackTransport(RuntimeEndpoint& owner, RuntimeEndpoint& remote) + : m_owner(owner), + m_remote(remote) { } @@ -176,6 +177,7 @@ class LoopbackTransport : public ISnaccROSETransport } private: + RuntimeEndpoint& m_owner; // endpoint that performs outbound sends RuntimeEndpoint& m_remote; // peer endpoint that receives delivered payloads std::deque m_actions; // scripted behaviors consumed one send at a time std::vector m_queuedMessages; // delayed payloads kept until the test flushes them @@ -191,7 +193,7 @@ class IRuntimeModule // Returns the generated interface id used for dispatch lookup. virtual unsigned int InterfaceId() const = 0; // Hands the decoded ROSE message to the generated dispatcher owned by the module. - virtual long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; + virtual long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) = 0; }; // Test-specific invoke context that remembers both the local session owning the @@ -263,6 +265,170 @@ class SessionInvokeContext : public SnaccInvokeContext std::string m_strTelemetryNote; // extra test data used to verify clone and prepare semantics }; +// Copies invoke-context fields while the runtime reference is still alive. +class InvokeContextSnapshot +{ +public: + void Reset() + { + m_bCaptured = false; + m_bIsSessionContext = false; + m_strLocalSessionId.clear(); + m_strInvokeSessionId.clear(); + m_strTelemetryNote.clear(); + } + + void Capture(const SnaccInvokeContext& ctx) + { + m_bCaptured = true; + if (const auto* pSessionContext = dynamic_cast(&ctx)) + { + m_bIsSessionContext = true; + m_strLocalSessionId = pSessionContext->m_strLocalSessionId; + m_strInvokeSessionId = pSessionContext->m_strInvokeSessionId; + m_strTelemetryNote = pSessionContext->TelemetryNote(); + return; + } + + m_bIsSessionContext = false; + m_strLocalSessionId.clear(); + m_strInvokeSessionId.clear(); + m_strTelemetryNote.clear(); + } + + bool WasCaptured() const + { + return m_bCaptured; + } + + bool IsSessionContext() const + { + return m_bIsSessionContext; + } + + const std::string& LocalSessionId() const + { + return m_strLocalSessionId; + } + + const std::string& InvokeSessionId() const + { + return m_strInvokeSessionId; + } + + const std::string& TelemetryNote() const + { + return m_strTelemetryNote; + } + +private: + bool m_bCaptured = false; + bool m_bIsSessionContext = false; + std::string m_strLocalSessionId; + std::string m_strInvokeSessionId; + std::string m_strTelemetryNote; +}; + +// Records inbound handler and transport observations for one server endpoint. +class InboundInvokeObservation +{ +public: + void Reset() + { + m_handlerSnapshot.Reset(); + m_lastTransportSnapshot.Reset(); + m_pHandlerContextAddress = nullptr; + m_bSameInstanceHandlerToTransport = false; + m_nTransportSendCount = 0; + } + + void CaptureHandlerContext(SnaccInvokeContext& ctx) + { + m_handlerSnapshot.Capture(ctx); + m_pHandlerContextAddress = &ctx; + m_bSameInstanceHandlerToTransport = false; + } + + void CaptureTransportSend(SnaccInvokeContext& ctx) + { + m_lastTransportSnapshot.Capture(ctx); + ++m_nTransportSendCount; + if (m_pHandlerContextAddress != nullptr) + m_bSameInstanceHandlerToTransport = (&ctx == m_pHandlerContextAddress); + } + + const InvokeContextSnapshot& HandlerSnapshot() const + { + return m_handlerSnapshot; + } + + const InvokeContextSnapshot& LastTransportSnapshot() const + { + return m_lastTransportSnapshot; + } + + bool HandlerWasInvoked() const + { + return m_handlerSnapshot.WasCaptured(); + } + + bool SameInstanceHandlerToTransport() const + { + return m_bSameInstanceHandlerToTransport; + } + + size_t TransportSendCount() const + { + return m_nTransportSendCount; + } + +private: + InvokeContextSnapshot m_handlerSnapshot; + InvokeContextSnapshot m_lastTransportSnapshot; + SnaccInvokeContext* m_pHandlerContextAddress = nullptr; + bool m_bSameInstanceHandlerToTransport = false; + size_t m_nTransportSendCount = 0; +}; + +// Records outbound transport observations copied at send time. +class OutboundTransportObservation +{ +public: + void Reset() + { + m_lastTransportSnapshot.Reset(); + m_pLastContextAddress = nullptr; + m_nTransportSendCount = 0; + } + + void CaptureTransportSend(SnaccInvokeContext& ctx) + { + m_lastTransportSnapshot.Capture(ctx); + m_pLastContextAddress = &ctx; + ++m_nTransportSendCount; + } + + const InvokeContextSnapshot& LastTransportSnapshot() const + { + return m_lastTransportSnapshot; + } + + const SnaccInvokeContext* LastContextAddress() const + { + return m_pLastContextAddress; + } + + size_t TransportSendCount() const + { + return m_nTransportSendCount; + } + +private: + InvokeContextSnapshot m_lastTransportSnapshot; + const SnaccInvokeContext* m_pLastContextAddress = nullptr; + size_t m_nTransportSendCount = 0; +}; + // Session-scoped runtime host used on both client and server sides. It models a // single ROSE connection, owns the transport binding, injects session ids, and // dispatches inbound invokes to the registered modules. @@ -281,10 +447,42 @@ class RuntimeEndpoint : public SnaccROSEBase // Attaches a loopback transport that forwards outbound data to the peer host. void ConnectTo(RuntimeEndpoint& remote) { - m_transport = std::make_unique(remote); + m_transport = std::make_unique(*this, remote); SetSnaccROSETransport(m_transport.get()); } + // Clears invoke-context observations recorded by the test harness. + void ResetInvokeContextObservations() + { + m_inboundObservation.Reset(); + m_outboundTransportObservation.Reset(); + } + + // Returns inbound handler and response-send observations for this endpoint. + const InboundInvokeObservation& InboundObservation() const + { + return m_inboundObservation; + } + + // Returns outbound transport observations copied at send time. + const OutboundTransportObservation& OutboundTransportObservationState() const + { + return m_outboundTransportObservation; + } + + // Records the invoke context seen inside an inbound handler callback. + void RecordInboundHandlerContext(SnaccInvokeContext& ctx) + { + m_inboundObservation.CaptureHandlerContext(ctx); + } + + // Records an outbound transport send on this endpoint. + void RecordTransportSend(SnaccInvokeContext& ctx) + { + m_inboundObservation.CaptureTransportSend(ctx); + m_outboundTransportObservation.CaptureTransportSend(ctx); + } + // Selects the wire encoding used for all outbound traffic on this host. void SetEncoding(const TransportEncoding encoding) { @@ -319,6 +517,11 @@ class RuntimeEndpoint : public SnaccROSEBase return *m_transport; } + const LoopbackTransport& Transport() const + { + return *m_transport; + } + // Injects already encoded transport data directly into the inbound pipeline. void ReceiveRaw(const std::string& payload) { @@ -338,33 +541,33 @@ class RuntimeEndpoint : public SnaccROSEBase } // Attaches the endpoint session id before the base runtime encodes an invoke. - long SendInvoke(SNACC::ROSEInvoke* pinvoke, SNACC::AsnType* result, SNACC::AsnType* error, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override + long SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResult, SNACC::AsnType* pError, const char* szOperationName, int iTimeout = -1, std::shared_ptr pCtx = {}) override { - AttachSessionId(pinvoke); - return SnaccROSEBase::SendInvoke(pinvoke, result, error, szOperationName, iTimeout, std::move(pCtx)); + AttachSessionId(pInvoke); + return SnaccROSEBase::SendInvoke(pInvoke, pResult, pError, szOperationName, iTimeout, std::move(pCtx)); } // Attaches the endpoint session id before the base runtime encodes an event. - long SendEvent(SNACC::ROSEInvoke* pinvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override + long SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx = {}) override { - AttachSessionId(pinvoke); - return SnaccROSEBase::SendEvent(pinvoke, szOperationName, std::move(pCtx)); + AttachSessionId(pInvoke); + return SnaccROSEBase::SendEvent(pInvoke, szOperationName, std::move(pCtx)); } // Ensures outbound result payloads inherit the session id of this connection. - long EncodeResult(unsigned int uiInvokeID, SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override + long EncodeResult(unsigned int uiInvokeID, const SNACC::AsnType* pResult, std::string& strResponse, const wchar_t* szSessionID = nullptr) override { return SnaccROSEBase::EncodeResult(uiInvokeID, pResult, strResponse, szSessionID ? szSessionID : m_wstrSessionId.c_str()); } // Ensures outbound error payloads inherit the session id of this connection. - long EncodeError(unsigned int uiInvokeID, SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override + long EncodeError(unsigned int uiInvokeID, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) override { return SnaccROSEBase::EncodeError(uiInvokeID, pError, strResponse, szSessionID ? szSessionID : m_wstrSessionId.c_str()); } // Generic interface-id based dispatcher used by all tests in this harness. - long OnInvoke(ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long OnInvoke(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { const unsigned int interfaceId = SnaccRoseOperationLookup::LookUpInterfaceID(pMsg->invoke->operationID); const auto it = m_modules.find(interfaceId); @@ -389,6 +592,8 @@ class RuntimeEndpoint : public SnaccROSEBase std::map m_modules; // registered modules keyed by generated interface id std::string m_strSessionId; // narrow session id used in test assertions and invoke payloads std::wstring m_wstrSessionId; // wide session id used by SnaccROSEBase result/error encoders + InboundInvokeObservation m_inboundObservation; // inbound handler and transport snapshots + OutboundTransportObservation m_outboundTransportObservation; // outbound transport snapshots }; // Runtime endpoint used by the test fixture when log assertions are required. @@ -546,14 +751,16 @@ class SettingsServiceModule : public ENetUC_Settings_ManagerROSEInterface, publi } // Calls the generated dispatcher for all settings traffic addressed to this module. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Settings_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } // Implements the sample "get settings" invoke on the server side. - InvokeResult OnInvoke_asnGetSettings(AsnGetSettingsArgument* /* argument */, AsnGetSettingsResult* result, AsnRequestError* error, SnaccInvokeContext& /* ctx */) override + InvokeResult OnInvoke_asnGetSettings(AsnGetSettingsArgument* /* argument */, AsnGetSettingsResult* result, AsnRequestError* error, SnaccInvokeContext& ctx) override { + m_endpoint.RecordInboundHandlerContext(ctx); + if (!m_handlerModes.implementGetSettings) return InvokeResult::returnReject; if (m_handlerModes.getSettingsReturnsError) @@ -632,7 +839,7 @@ class SettingsClientModule : public ENetUC_Settings_ManagerROSEInterface, public } // Uses the generated settings dispatcher for inbound results/events on the client. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Settings_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } @@ -748,7 +955,7 @@ class EventServiceModule : public ENetUC_Event_ManagerROSEInterface, public IRun } // Calls the generated dispatcher for inbound event-manager traffic. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Event_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } @@ -815,7 +1022,7 @@ class EventClientModule : public ENetUC_Event_ManagerROSEInterface, public IRunt } // Uses the generated event-manager dispatcher for inbound notifications. - long Dispatch(SNACC::ROSEMessage* pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override + long Dispatch(std::unique_ptr& pMsg, SnaccInvokeContext& ctx, std::string& strResponse) override { return ENetUC_Event_ManagerROSE::OnInvoke(pMsg, &m_endpoint, this, ctx, strResponse); } @@ -866,8 +1073,10 @@ class EventClientModule : public ENetUC_Event_ManagerROSEInterface, public IRunt // Applies the scripted transport behavior and either delivers, queues, drops, or // corrupts the outbound payload. -inline long LoopbackTransport::SendBinaryDataBlockEx(const char* lpBytes, size_t size, SnaccInvokeContext& /* ctx */) +inline long LoopbackTransport::SendBinaryDataBlockEx(const char* lpBytes, size_t size, SnaccInvokeContext& ctx) { + m_owner.RecordTransportSend(ctx); + TransportAction action; if (!m_actions.empty()) { @@ -947,6 +1156,8 @@ class RuntimeTestBase : public ::testing::Test m_clientSettingsModule.ResetState(); m_serverEventModule.ResetState(); m_clientEventModule.ResetState(); + m_server.ResetInvokeContextObservations(); + m_client.ResetInvokeContextObservations(); m_server.SetLogLevels(EAsnLogLevel::DISABLED, EAsnLogLevel::DISABLED); m_client.SetLogLevels(EAsnLogLevel::DISABLED, EAsnLogLevel::DISABLED); m_server.SetForwardLogsToBase(false); @@ -1047,6 +1258,18 @@ class RuntimeTestBase : public ::testing::Test return m_telemetryRecorder.Snapshot(); } + // Returns inbound invoke-context observations recorded on the server endpoint. + const InboundInvokeObservation& ServerInboundObservation() const + { + return m_server.InboundObservation(); + } + + // Returns outbound transport observations recorded on the client endpoint. + const OutboundTransportObservation& ClientOutboundTransportObservation() const + { + return m_client.OutboundTransportObservationState(); + } + // Gives tests a simple way to create a session-aware outbound context for the client. std::shared_ptr CreateClientInvokeContext(SNACC::ROSEInvoke* pInvoke, const char* szOperationName) { diff --git a/version.h b/version.h index 225f4f0..357e845 100644 --- a/version.h +++ b/version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "7.0.5" -#define VERSION_RC 7, 0, 5 -#define RELDATE "03.07.2026" +#define VERSION "7.0.6" +#define VERSION_RC 7, 0, 6 +#define RELDATE "07.07.2026" #endif // VERSION_H