diff --git a/.cursor/rules/interface-negotiate-cpp-ts-parity.mdc b/.cursor/rules/interface-negotiate-cpp-ts-parity.mdc new file mode 100644 index 0000000..4abc94a --- /dev/null +++ b/.cursor/rules/interface-negotiate-cpp-ts-parity.mdc @@ -0,0 +1,32 @@ +--- +description: Interface negotiate and remote-offer gating — keep C++ and TypeScript gluecode in sync +globs: cpp-lib/**/*,compiler/back-ends/ts-gen/**/* +alwaysApply: false +--- + +# Interface negotiate — C++ and TypeScript together + +Applies to **remote capabilities**, **asnNegotiateInterface**, and **client-side gating** of outbound invokes based on what the peer advertised. + +## Paired surfaces (change both in one task) + +| C++ (`cpp-lib/`) | TypeScript (`compiler/back-ends/ts-gen/gluecode/`) | +|------------------|------------------------------------------------------| +| `ISnaccRoseSessionSubscription`, `SnaccROSESender`, `SnaccROSEComponent` | `IRoseSessionSubscription`, `IASN1Transport`, `ROSEBase`, `RoseSessionSubscriptionStore` | +| `SetOperationBlockPolicy` / `IsOperationBlocked` | `setOperationBlockPolicy` / `isOperationBlocked` | +| `CompleteIfProcessingShutdown` / `CompleteIfOperationBlocked` on `SendEvent` / `SendInvoke` | `completeIfProcessingShutdown` / `completeIfOperationBlocked` on `handleEvent` / `handleInvoke` and `sendInvoke` | +| `PauseRoseProcessing` / `ResumeRoseProcessing` / `LookUpInterfaceID` on `SnaccROSEBase` | `pauseRoseProcessing` / `resumeRoseProcessing` / `lookUpInterfaceID` on `TSASN1Base` | +| `SnaccROSEBase`, `SnaccModuleCapabilities`, `SnaccRoseOperationLookup` | `TSASN1Base`, `TSModuleCapabilities`, `TSROSEBase` | +| `cpp-lib/tests/` (runtime / capability tests) | `compiler/back-ends/ts-gen/tests/` | + +Do **not** land C++-only or TS-only halves of the same behavior. Tests on both sides move together when semantics change. + +## Naming + +- **Ask the user** before introducing or renaming public API (types, enums, methods, flags). +- Propose 1–2 options with a one-line rationale; **do not implement renames** until they pick one. + +## Branching + +- Feature work on a ticket branch (e.g. `feature/UCAAS-1486`) in this repo. +- ProCall / `global` pin updates follow after the agreed API is stable — not as a workaround for missing TS or C++ work here. diff --git a/compiler/back-ends/ts-gen/gen-ts-combined.c b/compiler/back-ends/ts-gen/gen-ts-combined.c index 17b233d..d563700 100644 --- a/compiler/back-ends/ts-gen/gen-ts-combined.c +++ b/compiler/back-ends/ts-gen/gen-ts-combined.c @@ -65,6 +65,26 @@ void PrintTSRootTypes(FILE* src, Module* mod, const char* szSuffix) fprintf(src, "export const MODULE_NAME = \"%s\";\n", mod->moduleName); + { + ValueDef* vd; + int iFirstIIDFound = 0; + FOR_EACH_LIST_ELMT(vd, mod->valueDefs) + { + if (vd->value->basicValue->choiceId != BASICVALUE_INTEGER) + continue; + if (vd->value->type->basicType->choiceId != BASICTYPE_MACROTYPE) + continue; + if (vd->value->type->basicType->a.macroType->choiceId != MACROTYPE_ROSOPERATION) + continue; + if (!iFirstIIDFound) + { + iFirstIIDFound = 1; + fprintf(src, "export const MODULE_IID = %d;\n", vd->value->basicValue->a.integer); + } + break; + } + } + if (gMajorInterfaceVersion >= 0) { long long lMinorModuleVersion = GetModulePatchVersion(mod->moduleName); diff --git a/compiler/back-ends/ts-gen/gen-ts-rose.c b/compiler/back-ends/ts-gen/gen-ts-rose.c index 038a4bf..8964824 100644 --- a/compiler/back-ends/ts-gen/gen-ts-rose.c +++ b/compiler/back-ends/ts-gen/gen-ts-rose.c @@ -78,6 +78,18 @@ void SaveTSROSEFilesToOutputDirectory(const int genRoseStubs, const char* szPath strcat_s(szFileName, _MAX_PATH - 1, "TSROSEBase.ts"); SaveResourceToFile(ETS_ROSE_BASE, szFileName); } + { + char szFileName[_MAX_PATH] = {0}; + strcpy_s(szFileName, _MAX_PATH - 1, szPath); + strcat_s(szFileName, _MAX_PATH - 1, "IRoseSessionSubscription.ts"); + SaveResourceToFile(ETS_ROSE_SESSION_SUBSCRIPTION, szFileName); + } + { + char szFileName[_MAX_PATH] = {0}; + strcpy_s(szFileName, _MAX_PATH - 1, szPath); + strcat_s(szFileName, _MAX_PATH - 1, "RoseSessionSubscriptionStore.ts"); + SaveResourceToFile(ETS_ROSE_SESSION_SUBSCRIPTION_STORE, szFileName); + } { char szFileName[_MAX_PATH] = {0}; strcpy_s(szFileName, _MAX_PATH - 1, szPath); @@ -498,10 +510,11 @@ void PrintTSROSESetHandler(FILE* src, Module* m) fprintf( src, - "\t\tthis.transport.registerOperation(this, handler, OperationIDs.OPID_%s, \"%s\", %s.MODULE_NAME, %lld, %lld, %s);\n", + "\t\tthis.transport.registerOperation(this, handler, OperationIDs.OPID_%s, \"%s\", %s.MODULE_NAME, %s.MODULE_IID, %lld, %lld, %s);\n", vd->definedName, vd->definedName, GetNameSpace(m), + GetNameSpace(m), llAddedUnix, llDeprecatedUnix, bIsEvent ? "true" : "false"); diff --git a/compiler/back-ends/ts-gen/gluecode/IRoseSessionSubscription.ts b/compiler/back-ends/ts-gen/gluecode/IRoseSessionSubscription.ts new file mode 100644 index 0000000..584a408 --- /dev/null +++ b/compiler/back-ends/ts-gen/gluecode/IRoseSessionSubscription.ts @@ -0,0 +1,22 @@ +/* + * Per-session ROSE subscription contract (parity with C++ ISnaccRoseSessionSubscription). + * + * Naming map for future backends: + * C++ clearAllSubscriptions / setSubscribedEvents / isSubscribedEvent + * TS clearAllSubscriptions / setSubscribedEvents / isSubscribedEvent + * + * Server-side transports must override all methods. TSASN1Base defaults call snaccAssertFail. + * When OperationBlockPolicy is BlockUnsupportedOperations and session state is marked, + * gluecode blocks outbound traffic via isOperationBlocked at handleEvent / handleInvoke. + */ +export interface IRoseSessionSubscription { + clearAllSubscriptions(): void; + clearSubscribedEvents(moduleIid: number): void; + clearSupportedInvokes(moduleIid: number): void; + setSubscribedEvents(moduleIid: number, eventOpIds: readonly number[]): void; + addSubscribedEvent(moduleIid: number, eventOpId: number): void; + setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void; + addSupportedInvoke(moduleIid: number, invokeOpId: number): void; + isSubscribedEvent(eventOpId: number): boolean; + isSupportedInvoke(invokeOpId: number): boolean; +} diff --git a/compiler/back-ends/ts-gen/gluecode/RoseSessionSubscriptionStore.ts b/compiler/back-ends/ts-gen/gluecode/RoseSessionSubscriptionStore.ts new file mode 100644 index 0000000..9dee914 --- /dev/null +++ b/compiler/back-ends/ts-gen/gluecode/RoseSessionSubscriptionStore.ts @@ -0,0 +1,82 @@ +import type { IRoseSessionSubscription } from "./IRoseSessionSubscription.js"; + +/* + * In-memory per-session subscription store (reference implementation for TS servers). + * Parity with UCServer ENetCtiSessionSubscriptionStore; reuse the same shape for Kotlin/Swift ports. + */ +export class RoseSessionSubscriptionStore implements IRoseSessionSubscription { + private subscribedEventsByModule = new Map>(); + private subscribedEventOpIds = new Set(); + private supportedInvokesByModule = new Map>(); + private supportedInvokeOpIds = new Set(); + + public clearAllSubscriptions(): void { + this.subscribedEventsByModule.clear(); + this.subscribedEventOpIds.clear(); + this.supportedInvokesByModule.clear(); + this.supportedInvokeOpIds.clear(); + } + + public clearSubscribedEvents(moduleIid: number): void { + this.clearModuleOpIds(this.subscribedEventsByModule, this.subscribedEventOpIds, moduleIid); + } + + public clearSupportedInvokes(moduleIid: number): void { + this.clearModuleOpIds(this.supportedInvokesByModule, this.supportedInvokeOpIds, moduleIid); + } + + public setSubscribedEvents(moduleIid: number, eventOpIds: readonly number[]): void { + this.replaceModuleOpIds(this.subscribedEventsByModule, this.subscribedEventOpIds, moduleIid, eventOpIds); + } + + public addSubscribedEvent(moduleIid: number, eventOpId: number): void { + this.subscribedEventsByModule.set(moduleIid, this.subscribedEventsByModule.get(moduleIid) ?? new Set()); + this.subscribedEventsByModule.get(moduleIid)!.add(eventOpId); + this.subscribedEventOpIds.add(eventOpId); + } + + public setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void { + this.replaceModuleOpIds(this.supportedInvokesByModule, this.supportedInvokeOpIds, moduleIid, invokeOpIds); + } + + public addSupportedInvoke(moduleIid: number, invokeOpId: number): void { + this.supportedInvokesByModule.set(moduleIid, this.supportedInvokesByModule.get(moduleIid) ?? new Set()); + this.supportedInvokesByModule.get(moduleIid)!.add(invokeOpId); + this.supportedInvokeOpIds.add(invokeOpId); + } + + public isSubscribedEvent(eventOpId: number): boolean { + return this.subscribedEventOpIds.has(eventOpId); + } + + public isSupportedInvoke(invokeOpId: number): boolean { + return this.supportedInvokeOpIds.has(invokeOpId); + } + + private replaceModuleOpIds( + moduleMap: Map>, + flatOpIds: Set, + moduleIid: number, + opIds: readonly number[], + ): void { + this.clearModuleOpIds(moduleMap, flatOpIds, moduleIid); + if (opIds.length === 0) + return; + + const moduleOpIds = new Set(); + for (const opId of opIds) { + moduleOpIds.add(opId); + flatOpIds.add(opId); + } + moduleMap.set(moduleIid, moduleOpIds); + } + + private clearModuleOpIds(moduleMap: Map>, flatOpIds: Set, moduleIid: number): void { + const previous = moduleMap.get(moduleIid); + if (!previous) + return; + for (const opId of previous) + flatOpIds.delete(opId); + moduleMap.delete(moduleIid); + } +} diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts index a2769c3..cd7788c 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Base.ts @@ -29,9 +29,10 @@ import { IROSELogger, ISendInvokeContext, ReceiveInvokeContext, - RemoteCapabilityMode, + OperationBlockPolicy, snaccAssert, snaccAssertFail, + ROSE_TE_SHUTDOWN, ASN1ByteArray, ROSEBase, } from "./TSROSEBase.js"; @@ -65,6 +66,8 @@ class Handler { public readonly operationName: string; // ASN.1 module that owns this operation public readonly moduleName: string; + // Generated module interface id (m_iid / MODULE_IID) + public readonly moduleInterfaceId: number; // @added unix timestamp from ASN.1 comments (0 = none) public readonly addedUnix: number; // @deprecated unix timestamp from ASN.1 comments (0 = none) @@ -88,6 +91,7 @@ class Handler { operationID: number, operationName: string, moduleName: string, + moduleInterfaceId: number, addedUnix: number, deprecatedUnix: number, isEvent: boolean, @@ -97,6 +101,7 @@ class Handler { this.operationID = operationID; this.operationName = operationName; this.moduleName = moduleName; + this.moduleInterfaceId = moduleInterfaceId; this.addedUnix = addedUnix; this.deprecatedUnix = deprecatedUnix; this.isEvent = isEvent; @@ -220,6 +225,20 @@ export class PendingInvoke { this.resolve(reject); } + /** + * Called from TSASN1Base when pauseRoseProcessing() completes pending operations with ROSE_TE_SHUTDOWN. + */ + public completed_shutdown(): void { + this.clearTimeout(); + const reject = new ROSEReject({ + invokedID: { invokedID: this.invoke.invokeID }, + sessionID: this.invoke.sessionID, + details: "transport shutdown", + reject: { invokeProblem: ROSE_TE_SHUTDOWN }, + }); + this.resolve(reject); + } + /** * Called from the TSASN1Base if a timeout occured and the regular answer has not jet been provided */ @@ -307,7 +326,10 @@ export abstract class TSASN1Base implements IASN1Transport { // Peer negotiate snapshot applied on this stub (client/server outbound gating) private remoteModuleCapabilitiesByName = new Map(); private remoteModuleCapabilitiesSet = false; - private remoteCapabilityMode = RemoteCapabilityMode.Disabled; + private operationBlockPolicy = OperationBlockPolicy.NeverBlock; + private sessionSubscriptionStateSet = false; + private roseProcessingAllowed = true; + private interfaceIdsByOpId = new Map(); // The Logger Callback which must be set with the SetLogger Method protected logger?: IROSELogger; // Logs the raw transport (inbound before decoding, outbound after encoding) @@ -385,6 +407,7 @@ export abstract class TSASN1Base implements IASN1Transport { operationID: number, operationName: string, moduleName: string, + moduleInterfaceId: number, addedUnix: number, deprecatedUnix: number, isEvent: boolean, @@ -396,12 +419,14 @@ export abstract class TSASN1Base implements IASN1Transport { operationID, operationName, moduleName, + moduleInterfaceId, addedUnix, deprecatedUnix, isEvent, ); this.handlersByID.set(operationID, handler); this.handlersByName.set(operationName, handler); + this.interfaceIdsByOpId.set(operationID, moduleInterfaceId); this.trackRegisteredOperation(operationID, operationName, moduleName, addedUnix, deprecatedUnix, isEvent); } else { // trying to re-register a handler for an already registered operationID, this should not happen and indicates a problem in the calling code @@ -447,6 +472,7 @@ export abstract class TSASN1Base implements IASN1Transport { if (handler) { this.handlersByID.delete(operationID); this.handlersByName.delete(handler.operationName); + this.interfaceIdsByOpId.delete(operationID); const module = this.loadedModulesByName.get(handler.moduleName); if (module) { if (handler.isEvent) @@ -530,23 +556,148 @@ export abstract class TSASN1Base implements IASN1Transport { } /** - * Configures whether outbound invokes are gated on a negotiate snapshot. Default Disabled. + * Resolves generated module interface id (MODULE_IID) from operation id via the registry. + */ + public lookUpInterfaceID(operationID: number): number { + return this.interfaceIdsByOpId.get(operationID) ?? 0; + } + + /** + * Ends the current transport ROSE session: blocks new work and completes pending invokes with shutdown. + */ + public pauseRoseProcessing(): void { + this.roseProcessingAllowed = false; + this.completeAllPendingOperations(); + } + + /** + * Re-opens ROSE processing after pauseRoseProcessing() (e.g. transport reconnect). + */ + public resumeRoseProcessing(): void { + this.roseProcessingAllowed = true; + } + + /** + * False while pauseRoseProcessing() shutdown gate is active for this transport session. + */ + public isProcessingAllowed(): boolean { + return this.roseProcessingAllowed; + } + + /** Clears all subscribed events and supported invokes for this session. Server transports must override. */ + public clearAllSubscriptions(): void { + snaccAssertFail("clearAllSubscriptions not implemented on this transport"); + } + + /** Removes subscribed server-to-client event OPIDs for moduleIid only. Server transports must override. */ + public clearSubscribedEvents(_moduleIid: number): void { + snaccAssertFail("clearSubscribedEvents not implemented on this transport"); + } + + /** Removes supported server-to-client invoke OPIDs for moduleIid only. Server transports must override. */ + public clearSupportedInvokes(_moduleIid: number): void { + snaccAssertFail("clearSupportedInvokes not implemented on this transport"); + } + + /** Replaces the subscribed event OPID set for moduleIid. Server transports must override. */ + public setSubscribedEvents(_moduleIid: number, _eventOpIds: readonly number[]): void { + snaccAssertFail("setSubscribedEvents not implemented on this transport"); + } + + /** Adds one subscribed event OPID for moduleIid. Server transports must override. */ + public addSubscribedEvent(_moduleIid: number, _eventOpId: number): void { + snaccAssertFail("addSubscribedEvent not implemented on this transport"); + } + + /** Replaces the supported invoke OPID set for moduleIid. Server transports must override. */ + public setSupportedInvokes(_moduleIid: number, _invokeOpIds: readonly number[]): void { + snaccAssertFail("setSupportedInvokes not implemented on this transport"); + } + + /** Adds one supported invoke OPID for moduleIid. Server transports must override. */ + public addSupportedInvoke(_moduleIid: number, _invokeOpId: number): void { + snaccAssertFail("addSupportedInvoke not implemented on this transport"); + } + + /** True when eventOpId is in the effective subscribed-event set. Server transports must override. */ + public isSubscribedEvent(_eventOpId: number): boolean { + snaccAssertFail("isSubscribedEvent not implemented on this transport"); + return false; + } + + /** True when invokeOpId is in the supported server-to-client invoke set. Server transports must override. */ + public isSupportedInvoke(_invokeOpId: number): boolean { + snaccAssertFail("isSupportedInvoke not implemented on this transport"); + return false; + } + + /** + * Configures blocking of outbound operations not covered by session subscription or negotiate state. Default NeverBlock. */ - public setRemoteCapabilityMode(mode: RemoteCapabilityMode): void { - this.remoteCapabilityMode = mode; + public setOperationBlockPolicy(policy: OperationBlockPolicy): void { + this.operationBlockPolicy = policy; } /** - * Returns the current remote capability gating mode for outbound invokes. + * Returns the current outbound operation block policy. */ - public getRemoteCapabilityMode(): RemoteCapabilityMode { - return this.remoteCapabilityMode; + public getOperationBlockPolicy(): OperationBlockPolicy { + return this.operationBlockPolicy; + } + + /** + * True after markSessionSubscriptionStateSet() (typically when subscribe handlers update session OPIDs). + */ + public hasSessionSubscriptionState(): boolean { + return this.sessionSubscriptionStateSet; + } + + /** + * Marks session subscription state as initialized; call from setSubscribedEvents and related overrides. + */ + public markSessionSubscriptionStateSet(): void { + this.sessionSubscriptionStateSet = true; + } + + /** + * True when BlockUnsupportedOperations is active, capability state is set, and the outbound op is not allowed. + */ + public isOperationBlocked(operationID: number, isEvent: boolean): boolean { + if (this.operationBlockPolicy !== OperationBlockPolicy.BlockUnsupportedOperations) + return false; + + if (isEvent) { + if (!this.sessionSubscriptionStateSet) + return false; + if (this.isSubscribedEvent(operationID)) + return false; + snaccAssertFail( + `Outbound event blocked: operation id ${operationID} is not subscribed for this session`, + ); + return true; + } + + if (this.sessionSubscriptionStateSet && !this.isSupportedInvoke(operationID)) { + snaccAssertFail( + `Outbound invoke blocked: operation id ${operationID} is not supported for this session`, + ); + return true; + } + + if (this.remoteModuleCapabilitiesSet && !this.internalIsRemoteOperationSupported(operationID)) { + snaccAssertFail( + `Outbound invoke blocked: operation id ${operationID} is not offered by the remote peer`, + ); + return true; + } + + return false; } /** * Stores the peer module snapshot from asnNegotiateInterface (or equivalent). */ - public applyRemoteModuleCapabilities(remote: ReadonlyMap): void { + public setRemoteModuleCapabilities(remote: ReadonlyMap): void { this.remoteModuleCapabilitiesByName = new Map( [...remote.entries()].map(([moduleName, moduleInfo]) => [ moduleName, @@ -570,7 +721,7 @@ export abstract class TSASN1Base implements IASN1Transport { } /** - * True after applyRemoteModuleCapabilities() was called (even when the map is empty). + * True after setRemoteModuleCapabilities() was called (even when the map is empty). */ public hasRemoteModuleCapabilities(): boolean { return this.remoteModuleCapabilitiesSet; @@ -583,32 +734,50 @@ export abstract class TSASN1Base implements IASN1Transport { public isSupportedOperation(operationID: number): boolean { snaccAssert( this.remoteModuleCapabilitiesSet, - "isSupportedOperation requires applyRemoteModuleCapabilities first", + "isSupportedOperation requires setRemoteModuleCapabilities first", ); return this.internalIsRemoteOperationSupported(operationID); } /** - * Local reject for outbound invokes blocked by remote capability gating. - * Events (invokeID 99999) are never gated here. + * Asserts via isOperationBlocked when BlockUnsupportedOperations is active. + * Returns a local remoteNotCapable reject when the stub must stop (events and invokes). Otherwise undefined. + * Call from handleEvent / handleInvoke before encodeInvoke (parity with C++ CompleteIfOperationBlocked). */ - protected tryRejectRemoteNotCapable(invoke: ROSEInvoke): ROSEReject | undefined { - if (invoke.invokeID === 99999) - return undefined; - if (this.remoteCapabilityMode !== RemoteCapabilityMode.Enabled || !this.remoteModuleCapabilitiesSet) - return undefined; - if (this.internalIsRemoteOperationSupported(invoke.operationID)) + public completeIfOperationBlocked( + operationID: number, + operationName: string, + isEvent: boolean, + invokeID = 0, + ): ROSEReject | undefined { + if (!this.isOperationBlocked(operationID, isEvent)) return undefined; - snaccAssertFail( - `Outbound invoke blocked: operation not offered by remote (${invoke.operationName}, ${invoke.operationID})`, - ); return createInvokeReject( - invoke, + { invokeID: isEvent ? 99999 : invokeID, operationID, operationName } as ROSEInvoke, CustomInvokeProblemEnum.remoteNotCapable, - `Operation ${invoke.operationName} (${invoke.operationID}) is not offered by the remote peer`, + `Operation ${operationName} (${operationID}) is not supported for outbound send`, ); } + /** + * When pauseRoseProcessing() is active: returns a local shutdown reject. Otherwise undefined. + * Call from handleInvoke / sendInvoke before encode or send (parity with C++ CompleteIfProcessingShutdown). + */ + public completeIfProcessingShutdown(invoke: ROSEInvoke): ROSEReject | undefined { + if (this.roseProcessingAllowed) + return undefined; + return createInvokeReject(invoke, ROSE_TE_SHUTDOWN, "ROSE transport processing is paused"); + } + + /** + * Completes all pending synchronous/async invokes with ROSE_TE_SHUTDOWN (parity with C++ CompleteAllPendingOperations). + */ + protected completeAllPendingOperations(): void { + for (const pending of this.pendingInvokes.values()) + pending.completed_shutdown(); + this.pendingInvokes.clear(); + } + /** * Returns true when the applied remote snapshot lists the invoke OPID for its module. */ @@ -761,6 +930,9 @@ export abstract class TSASN1Base implements IASN1Transport { * @returns undefined or, if bSendEventSynchronous has been set true when the event was sent */ public sendEvent(data: IASN1InvokeData): undefined | boolean { + if (!this.isProcessingAllowed()) + return data.invokeContext?.bSendEventSynchronous ? false : undefined; + if (data.invokeContext?.bSendEventSynchronous) return this.sendEventSync(data); else { @@ -853,8 +1025,6 @@ export abstract class TSASN1Base implements IASN1Transport { if (message.invoke.operationName) invokeContext.operationName = message.invoke.operationName; else { - // In case the client did not provide an operationName, look it up - // This only works if we have a registered handler for the operation const handler = this.getHandlerById(invokeContext.operationID); if (handler) invokeContext.operationName = handler.operationName; @@ -873,9 +1043,12 @@ export abstract class TSASN1Base implements IASN1Transport { else this.logTransport(rawData, "receive", "in", invokeContext); - if (message.invoke) - result = await this.onROSEInvoke(message.invoke, invokeContext); - else if (message.result) + if (message.invoke) { + if (!this.isProcessingAllowed()) + result = createInvokeReject(message.invoke, ROSE_TE_SHUTDOWN, "ROSE transport processing is paused"); + else + result = await this.onROSEInvoke(message.invoke, invokeContext); + } else if (message.result) result = await this.onROSEResult(message.result, invokeContext); else if (message.error) result = await this.onROSEError(message.error, invokeContext); diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts index 1d675d7..9377435 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Client.ts @@ -271,9 +271,9 @@ export abstract class TSASN1Client extends TSASN1Base implements IASN1Transport * If no timeout was specified we resolve in undefined to cleanup the promise object */ public async sendInvoke(data: IASN1InvokeData): Promise { - const localReject = this.tryRejectRemoteNotCapable(data.invoke); - if (localReject) - return localReject; + const shutdownReject = this.completeIfProcessingShutdown(data.invoke); + if (shutdownReject) + return shutdownReject; return new Promise((resolve): void => { let resolveUndefined = true; diff --git a/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts b/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts index fa235a1..71903b8 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSASN1Server.ts @@ -70,6 +70,9 @@ export class TSASN1Server extends TSASN1Base implements IASN1Transport { * @returns true when the event was sent */ public sendEventSync(data: IASN1InvokeData): boolean { + if (!this.isProcessingAllowed()) + return false; + if (this.connectionhandler && data.invokeContext.clientConnectionID) { const client = this.connectionhandler.getClientConnection(data.invokeContext.clientConnectionID); if (client) { @@ -135,9 +138,9 @@ export class TSASN1Server extends TSASN1Base implements IASN1Transport { * If no timeout was specified we resolve in undefined to cleanup the promise object */ public async sendInvoke(data: IASN1InvokeData): Promise { - const localReject = this.tryRejectRemoteNotCapable(data.invoke); - if (localReject) - return localReject; + const shutdownReject = this.completeIfProcessingShutdown(data.invoke); + if (shutdownReject) + return shutdownReject; const clientConnectionID = data.invokeContext.clientConnectionID || data.invoke.sessionID; if (!clientConnectionID) { diff --git a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts index 2c8faf4..0ee1e6f 100644 --- a/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts +++ b/compiler/back-ends/ts-gen/gluecode/TSROSEBase.ts @@ -28,6 +28,7 @@ import { IReceiveInvokeContextParams, ISendInvokeContextParams, } from "./TSInvokeContext.js"; +import type { IRoseSessionSubscription } from "./IRoseSessionSubscription.js"; /** * The socket might be a node or browser websocket or a node raw tcp socket, thus we cast it to any @@ -72,6 +73,9 @@ export enum CustomInvokeProblemEnum { /** Client-local SendInvoke result: peer negotiate snapshot does not offer this invoke OPID. */ export const ROSE_REJECT_REMOTENOTCAPABLE = 0x00000E00; +/** Transport-layer shutdown (parity with C++ ROSE_TE_SHUTDOWN). */ +export const ROSE_TE_SHUTDOWN = 0x00000002; + /** * Debug-only assert with a human-readable message (console.assert in Node/browser). * Use snaccAssert(check, msg) for preconditions or snaccAssertFail(msg) when already in an error path. @@ -85,10 +89,10 @@ export function snaccAssertFail(message: string): void { console.assert(false, message); } -/** Controls outbound invoke gating against a negotiate snapshot on TSASN1Base. */ -export enum RemoteCapabilityMode { - Disabled = 0, - Enabled = 1, +/** Controls whether outbound operations are blocked when absent from the peer/session capability snapshot. */ +export enum OperationBlockPolicy { + NeverBlock = 0, + BlockUnsupportedOperations = 1, } /** @@ -479,7 +483,7 @@ export interface IInvokeHandler { /** * Defines the interface the transport layer has to fullfill */ -export interface IASN1Transport { +export interface IASN1Transport extends IRoseSessionSubscription { sendInvoke(data: IASN1InvokeData): Promise; sendEvent(data: IASN1InvokeData): undefined | boolean; registerOperation( @@ -488,6 +492,7 @@ export interface IASN1Transport { operationID: number, operationName: string, moduleName: string, + moduleInterfaceId: number, addedUnix: number, deprecatedUnix: number, isEvent: boolean, @@ -500,9 +505,23 @@ export interface IASN1Transport { lookUpName(operationID: number): string | undefined; lookUpID(operationName: string): number | undefined; lookUpModuleName(operationID: number): string | undefined; - setRemoteCapabilityMode(mode: RemoteCapabilityMode): void; - getRemoteCapabilityMode(): RemoteCapabilityMode; - applyRemoteModuleCapabilities(remote: ReadonlyMap): void; + lookUpInterfaceID(operationID: number): number; + pauseRoseProcessing(): void; + resumeRoseProcessing(): void; + isProcessingAllowed(): boolean; + setOperationBlockPolicy(policy: OperationBlockPolicy): void; + getOperationBlockPolicy(): OperationBlockPolicy; + hasSessionSubscriptionState(): boolean; + markSessionSubscriptionStateSet(): void; + isOperationBlocked(operationID: number, isEvent: boolean): boolean; + completeIfProcessingShutdown(invoke: ROSEInvoke): ROSEReject | undefined; + completeIfOperationBlocked( + operationID: number, + operationName: string, + isEvent: boolean, + invokeID?: number, + ): ROSEReject | undefined; + setRemoteModuleCapabilities(remote: ReadonlyMap): void; clearRemoteModuleCapabilities(): void; hasRemoteModuleCapabilities(): boolean; isSupportedOperation(operationID: number): boolean; @@ -742,6 +761,51 @@ export abstract class ROSEBase implements IASN1LogCallback { this.handleEvents = handleEvents; } + /** Forwards to transport; see IRoseSessionSubscription. */ + public setSubscribedEvents(moduleIid: number, opIds: readonly number[]): void { + this.transport.setSubscribedEvents(moduleIid, opIds); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public addSubscribedEvent(moduleIid: number, opId: number): void { + this.transport.addSubscribedEvent(moduleIid, opId); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public clearSubscribedEvents(moduleIid: number): void { + this.transport.clearSubscribedEvents(moduleIid); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public clearAllSubscriptions(): void { + this.transport.clearAllSubscriptions(); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public clearSupportedInvokes(moduleIid: number): void { + this.transport.clearSupportedInvokes(moduleIid); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void { + this.transport.setSupportedInvokes(moduleIid, invokeOpIds); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public addSupportedInvoke(moduleIid: number, invokeOpId: number): void { + this.transport.addSupportedInvoke(moduleIid, invokeOpId); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public isSubscribedEvent(opId: number): boolean { + return this.transport.isSubscribedEvent(opId); + } + + /** Forwards to transport; see IRoseSessionSubscription. */ + public isSupportedInvoke(opId: number): boolean { + return this.transport.isSupportedInvoke(opId); + } + /** * Starts event dispatching. Dispatches queued events first and then sets the flag to handle them directly. */ @@ -975,6 +1039,12 @@ export abstract class ROSEBase implements IASN1LogCallback { argumentConverter: IConverter, invokeContext?: ISendInvokeContextParams, ): undefined | boolean { + if (!this.transport.isProcessingAllowed()) + return invokeContext?.bSendEventSynchronous ? false : undefined; + + if (this.transport.completeIfOperationBlocked(operationID, operationName, true)) + return invokeContext?.bSendEventSynchronous ? false : undefined; + // Encodes the argument and the ROSEInvoke envelop const result = this.encodeInvoke(argument, operationID, operationName, argumentConverter, true, invokeContext); if (result instanceof AsnInvokeProblem) @@ -1008,6 +1078,18 @@ export abstract class ROSEBase implements IASN1LogCallback { invokeContext?: ISendInvokeContextParams, errorConverter: IConverter = ENetUC_Common_Converter.AsnRequestError_Converter, ): Promise { + const shutdownReject = this.transport.completeIfProcessingShutdown({ + invokeID: 0, + operationID, + operationName, + } as ROSEInvoke); + if (shutdownReject) + return handleRoseReject(shutdownReject); + + const blocked = this.transport.completeIfOperationBlocked(operationID, operationName, false); + if (blocked instanceof ROSEReject) + return handleRoseReject(blocked); + const result = this.encodeInvoke(argument, operationID, operationName, argumentConverter, false, invokeContext); if (result instanceof AsnInvokeProblem) return result; diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.invokeBlockPolicy.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.invokeBlockPolicy.test.ts new file mode 100644 index 0000000..d02f543 --- /dev/null +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.invokeBlockPolicy.test.ts @@ -0,0 +1,157 @@ +// Run: npx tsx compiler/back-ends/ts-gen/tests/TSASN1Base.invokeBlockPolicy.test.ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { + ASN1ClassInstanceType, + TSASN1Base, +} from "./workdir/TSASN1Base.js"; +import { RoseSessionSubscriptionStore } from "./workdir/RoseSessionSubscriptionStore.js"; +import { EASN1TransportEncoding } from "./workdir/TSInvokeContext.js"; +import { + CustomInvokeProblemEnum, + OperationBlockPolicy, + ROSE_REJECT_REMOTENOTCAPABLE, +} from "./workdir/TSROSEBase.js"; +import { buildRemoteModuleCapabilities } from "./workdir/TSModuleCapabilities.js"; +import type { IASN1InvokeData } from "./workdir/TSROSEBase.js"; +import type { ROSEError, ROSEInvoke, ROSEResult } from "./workdir/SNACCROSE.js"; +import { ROSEReject } from "./workdir/SNACCROSE.js"; + +class ClientTestTransport extends TSASN1Base { + public sendInvokeCount = 0; + + public constructor() { + super(EASN1TransportEncoding.JSON, ASN1ClassInstanceType.TSASN1NodeClient); + } + + public async sendInvoke(data: IASN1InvokeData): Promise { + ++this.sendInvokeCount; + return undefined; + } + + public sendEventSync(_data: IASN1InvokeData): boolean { + return true; + } + + public getSessionID(): string | undefined { + return undefined; + } +} + +class SubscriptionTransport extends TSASN1Base { + private readonly store = new RoseSessionSubscriptionStore(); + public sentEvents = 0; + + public constructor() { + super(EASN1TransportEncoding.JSON, ASN1ClassInstanceType.TSASN1Server); + } + + public setSubscribedEvents(moduleIid: number, eventOpIds: readonly number[]): void { + this.markSessionSubscriptionStateSet(); + this.store.setSubscribedEvents(moduleIid, eventOpIds); + } + + public setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void { + this.markSessionSubscriptionStateSet(); + this.store.setSupportedInvokes(moduleIid, invokeOpIds); + } + + public isSubscribedEvent(eventOpId: number): boolean { + return this.store.isSubscribedEvent(eventOpId); + } + + public isSupportedInvoke(invokeOpId: number): boolean { + return this.store.isSupportedInvoke(invokeOpId); + } + + public sendInvoke(data: IASN1InvokeData): Promise { + if (data.invoke.invokeID === 99999) + this.sentEvents++; + return Promise.resolve(undefined); + } + + public sendEventSync(_data: IASN1InvokeData): boolean { + this.sentEvents++; + return true; + } + + public getSessionID(): string | undefined { + return undefined; + } +} + +const noopHandler = { + getNameForOperationID: () => undefined, + getIDForOperationName: () => undefined, + onInvoke: async () => undefined, +}; + +function createInvoke(operationID: number, operationName: string, invokeID = 1): ROSEInvoke { + return { + invokeID, + operationID, + operationName, + } as ROSEInvoke; +} + +test("negotiate path: blockUnsupportedOperations without snapshot does not block sendInvoke", async () => { + const transport = new ClientTestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 100, 0, 0, false); + transport.setOperationBlockPolicy(OperationBlockPolicy.BlockUnsupportedOperations); + + await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + payLoad: new Uint8Array(), + } as IASN1InvokeData); + + assert.equal(transport.sendInvokeCount, 1); +}); + +test("negotiate path: unsupported op id returns remoteNotCapable reject at stub gate", () => { + const transport = new ClientTestTransport(); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 100, 0, 0, false); + transport.setRemoteModuleCapabilities(buildRemoteModuleCapabilities([ + { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, + ])); + transport.setOperationBlockPolicy(OperationBlockPolicy.BlockUnsupportedOperations); + + const reject = transport.completeIfOperationBlocked(100, "asnInvoke", false); + assert.ok(reject instanceof ROSEReject); + assert.equal(reject.reject.invokeProblem, CustomInvokeProblemEnum.remoteNotCapable); + assert.equal(CustomInvokeProblemEnum.remoteNotCapable, ROSE_REJECT_REMOTENOTCAPABLE); +}); + +test("subscription path: BlockUnsupportedOperations without state does not block", () => { + const transport = new SubscriptionTransport(); + transport.setOperationBlockPolicy(OperationBlockPolicy.BlockUnsupportedOperations); + assert.equal(transport.isOperationBlocked(2109, true), false); + assert.equal(transport.isOperationBlocked(2109, false), false); +}); + +test("subscription path: blocks unsubscribed event after state is marked", () => { + const transport = new SubscriptionTransport(); + transport.setOperationBlockPolicy(OperationBlockPolicy.BlockUnsupportedOperations); + transport.setSubscribedEvents(100, [2170]); + assert.equal(transport.isOperationBlocked(2109, true), true); + assert.equal(transport.isOperationBlocked(2170, true), false); +}); + +test("completeIfOperationBlocked blocks unsubscribed event after state is marked", () => { + const transport = new SubscriptionTransport(); + transport.setOperationBlockPolicy(OperationBlockPolicy.BlockUnsupportedOperations); + transport.setSubscribedEvents(100, [2170]); + const blocked = transport.completeIfOperationBlocked(2109, "asnJournalEntryChanged", true); + assert.ok(blocked instanceof ROSEReject); + assert.equal(blocked.reject.invokeProblem, CustomInvokeProblemEnum.remoteNotCapable); + assert.equal(transport.completeIfOperationBlocked(2170, "asnJournalEntryChanged", true), undefined); +}); + +test("completeIfOperationBlocked returns remoteNotCapable for unsupported server invoke", () => { + const transport = new SubscriptionTransport(); + transport.setOperationBlockPolicy(OperationBlockPolicy.BlockUnsupportedOperations); + transport.setSupportedInvokes(100, [3001]); + const reject = transport.completeIfOperationBlocked(3002, "asnExampleInvoke", false, 42); + assert.ok(reject instanceof ROSEReject); + assert.equal(reject.reject.invokeProblem, CustomInvokeProblemEnum.remoteNotCapable); +}); diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.pauseRoseProcessing.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.pauseRoseProcessing.test.ts new file mode 100644 index 0000000..35d0441 --- /dev/null +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.pauseRoseProcessing.test.ts @@ -0,0 +1,108 @@ +// Run: npx tsx compiler/back-ends/ts-gen/tests/TSASN1Base.pauseRoseProcessing.test.ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { + ASN1ClassInstanceType, + PendingInvoke, + TSASN1Base, +} from "./workdir/TSASN1Base.js"; +import { EASN1TransportEncoding } from "./workdir/TSInvokeContext.js"; +import { + ReceiveInvokeContext, + ROSE_TE_SHUTDOWN, + type IASN1InvokeData, +} from "./workdir/TSROSEBase.js"; +import { ROSEInvoke, type ROSEError, type ROSEReject, type ROSEResult } from "./workdir/SNACCROSE.js"; + +class TestTransport extends TSASN1Base { + public constructor() { + super(EASN1TransportEncoding.JSON, ASN1ClassInstanceType.TSASN1NodeClient); + } + + public async sendInvoke(data: IASN1InvokeData): Promise { + const shutdownReject = this.completeIfProcessingShutdown(data.invoke); + if (shutdownReject) + return shutdownReject; + return undefined; + } + + public sendEventSync(_data: IASN1InvokeData): boolean { + return true; + } + + public getSessionID(): string | undefined { + return undefined; + } +} + +class PendingTestTransport extends TestTransport { + public async sendInvoke(data: IASN1InvokeData): Promise { + const shutdownReject = this.completeIfProcessingShutdown(data.invoke); + if (shutdownReject) + return shutdownReject; + + return new Promise((resolve) => { + if (data.invoke.invokeID !== 99999) + this.pendingInvokes.set(data.invoke.invokeID, new PendingInvoke(data.invoke, resolve)); + else + resolve(undefined); + }); + } +} + +function createInvoke(operationID: number, operationName: string, invokeID = 1): ROSEInvoke { + return { + invokeID, + operationID, + operationName, + } as ROSEInvoke; +} + +test("pauseRoseProcessing blocks outbound invoke and event sends", async () => { + const transport = new TestTransport(); + transport.pauseRoseProcessing(); + + const invokeReject = await transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke"), + payLoad: {}, + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + }); + assert.equal(invokeReject?.reject?.invokeProblem, ROSE_TE_SHUTDOWN); + + const eventResult = transport.sendEvent({ + invoke: createInvoke(200, "asnEvent", 99999), + payLoad: {}, + invokeContext: transport.getInvokeContextParams(undefined, 200, "asnEvent", true), + }); + assert.equal(eventResult, undefined); + + transport.resumeRoseProcessing(); + assert.equal(transport.isProcessingAllowed(), true); +}); + +test("pauseRoseProcessing completes pending invokes with shutdown", async () => { + const transport = new PendingTestTransport(); + const pending = transport.sendInvoke({ + invoke: createInvoke(100, "asnInvoke", 7), + payLoad: {}, + invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), + }); + + transport.pauseRoseProcessing(); + const result = await pending; + assert.equal(result?.reject?.invokeProblem, ROSE_TE_SHUTDOWN); +}); + +test("receiveHandleROSEMessage rejects inbound invoke while paused", async () => { + const transport = new TestTransport(); + transport.pauseRoseProcessing(); + + const response = await transport.receiveHandleROSEMessage( + { invoke: createInvoke(100, "asnInvoke", 3) }, + {}, + new ReceiveInvokeContext({ encoding: EASN1TransportEncoding.JSON }), + ); + + assert.ok(response); + assert.match(String(response?.payLoad), /"invokeProblem":2/); +}); diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts index 7b4724c..eca7320 100644 --- a/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.registry.test.ts @@ -36,8 +36,8 @@ const noopHandler = { test("registerOperation metadata appears in getLoadedModules", () => { const transport = new TestTransport(); transport.registerModuleVersion("TestModule", "20240101.0.20240506"); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 1714968000, 0, false); - transport.registerOperation(noopHandler, noopHandler as never, 200, "asnEvent", "TestModule", 0, 1715054400, true); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 100, 1714968000, 0, false); + transport.registerOperation(noopHandler, noopHandler as never, 200, "asnEvent", "TestModule", 100, 0, 1715054400, true); const modules = transport.getLoadedModules(); assert.equal(modules.size, 1); @@ -59,10 +59,10 @@ test("separate stub instances keep separate registries", () => { const transportB = new TestTransport(); transportA.registerModuleVersion("ModuleA", "1.0.1"); - transportA.registerOperation(noopHandler, noopHandler as never, 10, "opA", "ModuleA", 0, 0, false); + transportA.registerOperation(noopHandler, noopHandler as never, 10, "opA", "ModuleA", 10, 0, 0, false); transportB.registerModuleVersion("ModuleB", "2.0.2"); - transportB.registerOperation(noopHandler, noopHandler as never, 20, "opB", "ModuleB", 0, 0, true); + transportB.registerOperation(noopHandler, noopHandler as never, 20, "opB", "ModuleB", 20, 0, 0, true); assert.equal(transportA.getLoadedModules().size, 1); assert.equal(transportB.getLoadedModules().size, 1); @@ -73,10 +73,11 @@ test("separate stub instances keep separate registries", () => { test("lookUpName lookUpID and lookUpModuleName resolve registered operations", () => { const transport = new TestTransport(); transport.registerModuleVersion("TestModule", "1.0.0"); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); + transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 100, 0, 0, false); assert.equal(transport.lookUpName(100), "asnInvoke"); assert.equal(transport.lookUpID("asnInvoke"), 100); assert.equal(transport.lookUpModuleName(100), "TestModule"); - assert.equal(transport.lookUpModuleName(999), undefined); + assert.equal(transport.lookUpInterfaceID(100), 100); + assert.equal(transport.lookUpInterfaceID(999), 0); }); diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts deleted file mode 100644 index 62e68ee..0000000 --- a/compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -// Run: npx tsx compiler/back-ends/ts-gen/tests/TSASN1Base.remoteCapability.test.ts -import assert from "node:assert/strict"; -import test from "node:test"; -import { - ASN1ClassInstanceType, - TSASN1Base, -} from "./workdir/TSASN1Base.js"; -import { EASN1TransportEncoding } from "./workdir/TSInvokeContext.js"; -import { - CustomInvokeProblemEnum, - RemoteCapabilityMode, - ROSE_REJECT_REMOTENOTCAPABLE, -} from "./workdir/TSROSEBase.js"; -import { buildRemoteModuleCapabilities } from "./workdir/TSModuleCapabilities.js"; -import type { IASN1InvokeData } from "./workdir/TSROSEBase.js"; -import type { ROSEError, ROSEInvoke, ROSEReject, ROSEResult } from "./workdir/SNACCROSE.js"; - -class TestTransport extends TSASN1Base { - public sendInvokeCount = 0; - - public constructor() { - super(EASN1TransportEncoding.JSON, ASN1ClassInstanceType.TSASN1NodeClient); - } - - public async sendInvoke(data: IASN1InvokeData): Promise { - const localReject = this.tryRejectRemoteNotCapable(data.invoke); - if (localReject) - return localReject; - ++this.sendInvokeCount; - return undefined; - } - - public sendEventSync(_data: IASN1InvokeData): boolean { - return true; - } - - public getSessionID(): string | undefined { - return undefined; - } -} - -const noopHandler = { - getNameForOperationID: () => undefined, - getIDForOperationName: () => undefined, - onInvoke: async () => undefined, -}; - -function createInvoke(operationID: number, operationName: string, invokeID = 1): ROSEInvoke { - return { - invokeID, - operationID, - operationName, - } as ROSEInvoke; -} - -test("lookUpName and lookUpModuleName resolve registered handlers", () => { - const transport = new TestTransport(); - transport.registerModuleVersion("TestModule", "1.0.0"); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - - assert.equal(transport.lookUpName(100), "asnInvoke"); - assert.equal(transport.lookUpID("asnInvoke"), 100); - assert.equal(transport.lookUpModuleName(100), "TestModule"); -}); - -test("enabled without snapshot does not gate sendInvoke", async () => { - const transport = new TestTransport(); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); - - await transport.sendInvoke({ - invoke: createInvoke(100, "asnInvoke"), - invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), - payLoad: new Uint8Array(), - } as IASN1InvokeData); - - assert.equal(transport.sendInvokeCount, 1); -}); - -test("enabled with unsupported op id returns remoteNotCapable reject", async () => { - const transport = new TestTransport(); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ - { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, - ])); - transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); - - const result = await transport.sendInvoke({ - invoke: createInvoke(100, "asnInvoke"), - invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), - payLoad: new Uint8Array(), - } as IASN1InvokeData); - - assert.ok(result); - assert.equal((result as ROSEReject).reject.invokeProblem, CustomInvokeProblemEnum.remoteNotCapable); - assert.equal(CustomInvokeProblemEnum.remoteNotCapable, ROSE_REJECT_REMOTENOTCAPABLE); - assert.equal(transport.sendInvokeCount, 0); -}); - -test("isSupportedOperation reflects applied snapshot", () => { - const transport = new TestTransport(); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ - { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [100] }, - ])); - - assert.equal(transport.hasRemoteModuleCapabilities(), true); - assert.equal(transport.isSupportedOperation(100), true); - assert.equal(transport.isSupportedOperation(200), false); -}); - -test("clearRemoteModuleCapabilities stops gating", async () => { - const transport = new TestTransport(); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ - { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, - ])); - transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); - transport.clearRemoteModuleCapabilities(); - - await transport.sendInvoke({ - invoke: createInvoke(100, "asnInvoke"), - invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), - payLoad: new Uint8Array(), - } as IASN1InvokeData); - - assert.equal(transport.sendInvokeCount, 1); -}); - -test("disabled with snapshot does not gate sendInvoke", async () => { - const transport = new TestTransport(); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ - { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [200] }, - ])); - transport.setRemoteCapabilityMode(RemoteCapabilityMode.Disabled); - - await transport.sendInvoke({ - invoke: createInvoke(100, "asnInvoke"), - invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), - payLoad: new Uint8Array(), - } as IASN1InvokeData); - - assert.equal(transport.sendInvokeCount, 1); -}); - -test("enabled with supported op id sends invoke", async () => { - const transport = new TestTransport(); - transport.registerOperation(noopHandler, noopHandler as never, 100, "asnInvoke", "TestModule", 0, 0, false); - transport.applyRemoteModuleCapabilities(buildRemoteModuleCapabilities([ - { moduleName: "TestModule", version: "1.0.0", invokeOpIds: [100] }, - ])); - transport.setRemoteCapabilityMode(RemoteCapabilityMode.Enabled); - - await transport.sendInvoke({ - invoke: createInvoke(100, "asnInvoke"), - invokeContext: transport.getInvokeContextParams(undefined, 100, "asnInvoke", false), - payLoad: new Uint8Array(), - } as IASN1InvokeData); - - assert.equal(transport.sendInvokeCount, 1); -}); diff --git a/compiler/back-ends/ts-gen/tests/TSASN1Base.roseSessionSubscription.test.ts b/compiler/back-ends/ts-gen/tests/TSASN1Base.roseSessionSubscription.test.ts new file mode 100644 index 0000000..c2839d8 --- /dev/null +++ b/compiler/back-ends/ts-gen/tests/TSASN1Base.roseSessionSubscription.test.ts @@ -0,0 +1,125 @@ +// Run: npx tsx compiler/back-ends/ts-gen/tests/TSASN1Base.roseSessionSubscription.test.ts +import assert from "node:assert/strict"; +import test from "node:test"; +import { + ASN1ClassInstanceType, + TSASN1Base, +} from "./workdir/TSASN1Base.js"; +import { RoseSessionSubscriptionStore } from "./workdir/RoseSessionSubscriptionStore.js"; +import { ROSEBase } from "./workdir/TSROSEBase.js"; +import { EASN1TransportEncoding } from "./workdir/TSInvokeContext.js"; +import type { IASN1InvokeData, IASN1LogData, IReceiveInvokeContext } from "./workdir/TSROSEBase.js"; +import type { ROSEError, ROSEInvoke, ROSEResult, ROSEReject } from "./workdir/SNACCROSE.js"; + +class SubscriptionTransport extends TSASN1Base { + private readonly store = new RoseSessionSubscriptionStore(); + public sentEvents = 0; + + public constructor() { + super(EASN1TransportEncoding.JSON, ASN1ClassInstanceType.TSASN1Server); + } + + public clearAllSubscriptions(): void { + this.store.clearAllSubscriptions(); + } + + public clearSubscribedEvents(moduleIid: number): void { + this.store.clearSubscribedEvents(moduleIid); + } + + public clearSupportedInvokes(moduleIid: number): void { + this.store.clearSupportedInvokes(moduleIid); + } + + public setSubscribedEvents(moduleIid: number, eventOpIds: readonly number[]): void { + this.store.setSubscribedEvents(moduleIid, eventOpIds); + } + + public addSubscribedEvent(moduleIid: number, eventOpId: number): void { + this.store.addSubscribedEvent(moduleIid, eventOpId); + } + + public setSupportedInvokes(moduleIid: number, invokeOpIds: readonly number[]): void { + this.store.setSupportedInvokes(moduleIid, invokeOpIds); + } + + public addSupportedInvoke(moduleIid: number, invokeOpId: number): void { + this.store.addSupportedInvoke(moduleIid, invokeOpId); + } + + public isSubscribedEvent(eventOpId: number): boolean { + return this.store.isSubscribedEvent(eventOpId); + } + + public isSupportedInvoke(invokeOpId: number): boolean { + return this.store.isSupportedInvoke(invokeOpId); + } + + public sendInvoke(_data: IASN1InvokeData): Promise { + return Promise.resolve(undefined); + } + + public sendEvent(_data: IASN1InvokeData): undefined { + this.sentEvents++; + return undefined; + } + + public sendEventSync(_data: IASN1InvokeData): boolean { + this.sentEvents++; + return true; + } + + public getSessionID(): string | undefined { + return undefined; + } +} + +class TestRoseComponent extends ROSEBase { + public readonly logFilter: string[] = []; + + public constructor(transport: SubscriptionTransport) { + super(transport, true); + } + + public getLogData(): IASN1LogData { + return { className: "TestRoseComponent" }; + } + + public getNameForOperationID(_id: number): string | undefined { + return undefined; + } + + public getIDForOperationName(_name: string): number | undefined { + return undefined; + } + + public async onInvoke( + _invoke: ROSEInvoke, + _invokeContext: IReceiveInvokeContext, + _handler: unknown, + ): Promise { + return undefined; + } +} + +test("RoseSessionSubscriptionStore tracks subscribed events", () => { + const store = new RoseSessionSubscriptionStore(); + assert.equal(store.isSubscribedEvent(2109), false); + store.addSubscribedEvent(2104, 2109); + assert.equal(store.isSubscribedEvent(2109), true); + assert.equal(store.isSubscribedEvent(2170), false); + store.clearSubscribedEvents(2104); + assert.equal(store.isSubscribedEvent(2109), false); +}); + +test("ROSEBase forwards subscription queries to transport", () => { + const transport = new SubscriptionTransport(); + const component = new TestRoseComponent(transport); + + assert.equal(component.isSubscribedEvent(2109), false); + component.addSubscribedEvent(2104, 2109); + assert.equal(component.isSubscribedEvent(2109), true); + assert.equal(component.isSupportedInvoke(4100), false); + component.addSupportedInvoke(2104, 4100); + assert.equal(component.isSupportedInvoke(4100), true); +}); diff --git a/compiler/compiler.rc b/compiler/compiler.rc index 2416eb0..983e7c9 100644 --- a/compiler/compiler.rc +++ b/compiler/compiler.rc @@ -36,6 +36,8 @@ TS_SNACCROSE RCDATA "back-ends/ts-gen/gluecode/SNACC TS_SNACCROSE_CONVERTER RCDATA "back-ends/ts-gen/gluecode/SNACCROSE_Converter.ts" TS_DEPRECATED_CALLBACK RCDATA "back-ends/ts-gen/gluecode/TSDeprecatedCallback.ts" TS_INVOKE_CONTEXT RCDATA "back-ends/ts-gen/gluecode/TSInvokeContext.ts" +TS_ROSE_SESSION_SUBSCRIPTION RCDATA "back-ends/ts-gen/gluecode/IRoseSessionSubscription.ts" +TS_ROSE_SESSION_SUBSCRIPTION_STORE RCDATA "back-ends/ts-gen/gluecode/RoseSessionSubscriptionStore.ts" DELPHI_ASN1_YPES RCDATA "back-ends/delphi-gen/gluecode/DelphiAsn1Types.pas" VS_VERSION_INFO VERSIONINFO diff --git a/compiler/core/efileressources.c b/compiler/core/efileressources.c index c0a50dd..a8c5e83 100644 --- a/compiler/core/efileressources.c +++ b/compiler/core/efileressources.c @@ -22,6 +22,8 @@ INCBIN(BIN_SNACCROSE, "compiler/back-ends/ts-gen/gluecode/SNACCROSE.ts"); INCBIN(BIN_SNACCROSE_CONVERTER, "compiler/back-ends/ts-gen/gluecode/SNACCROSE_Converter.ts"); INCBIN(BIN_INVOKE_CONTEXT, "compiler/back-ends/ts-gen/gluecode/TSInvokeContext.ts"); INCBIN(BIN_DEPRECATED_CALLBACK, "compiler/back-ends/ts-gen/gluecode/TSDeprecatedCallback.ts"); +INCBIN(BIN_ROSE_SESSION_SUBSCRIPTION, "compiler/back-ends/ts-gen/gluecode/IRoseSessionSubscription.ts"); +INCBIN(BIN_ROSE_SESSION_SUBSCRIPTION_STORE, "compiler/back-ends/ts-gen/gluecode/RoseSessionSubscriptionStore.ts"); INCBIN(BIN_EDELPHI_ASN1_YPES, "compiler/back-ends/delphi-gen/gluecode/DelphiAsn1Types.pas"); #endif // _WIN32 @@ -99,6 +101,12 @@ void SaveResourceToFile(enum EFILERESSOURCE resourceID, const char* szFileName) case ETS_INVOKE_CONTEXT: SaveIncBinToFile(gBIN_INVOKE_CONTEXTData, gBIN_INVOKE_CONTEXTSize, szFileName); break; + case ETS_ROSE_SESSION_SUBSCRIPTION: + SaveIncBinToFile(gBIN_ROSE_SESSION_SUBSCRIPTIONData, gBIN_ROSE_SESSION_SUBSCRIPTIONSize, szFileName); + break; + case ETS_ROSE_SESSION_SUBSCRIPTION_STORE: + SaveIncBinToFile(gBIN_ROSE_SESSION_SUBSCRIPTION_STOREData, gBIN_ROSE_SESSION_SUBSCRIPTION_STORESize, szFileName); + break; case EDELPHI_ASN1_TYPES: SaveIncBinToFile(gBIN_EDELPHI_ASN1_YPESData, gBIN_EDELPHI_ASN1_YPESSize, szFileName); break; diff --git a/compiler/core/efileressources.h b/compiler/core/efileressources.h index 64c5ddc..884223e 100644 --- a/compiler/core/efileressources.h +++ b/compiler/core/efileressources.h @@ -17,6 +17,8 @@ enum EFILERESSOURCE ETS_OPTIONALPARAM_CONVERTER = TS_OPTIONALPARAM_CONVERTER, ETS_DEPRECATED_CALLBACK = TS_DEPRECATED_CALLBACK, ETS_INVOKE_CONTEXT = TS_INVOKE_CONTEXT, + ETS_ROSE_SESSION_SUBSCRIPTION = TS_ROSE_SESSION_SUBSCRIPTION, + ETS_ROSE_SESSION_SUBSCRIPTION_STORE = TS_ROSE_SESSION_SUBSCRIPTION_STORE, EDELPHI_ASN1_TYPES = DELPHI_ASN1_TYPES, }; diff --git a/compiler/resource.h b/compiler/resource.h index ce41fca..c4e49d6 100644 --- a/compiler/resource.h +++ b/compiler/resource.h @@ -16,6 +16,8 @@ #define TS_DEPRECATED_CALLBACK 1010 #define TS_INVOKE_CONTEXT 1011 #define DELPHI_ASN1_TYPES 1012 +#define TS_ROSE_SESSION_SUBSCRIPTION 1013 +#define TS_ROSE_SESSION_SUBSCRIPTION_STORE 1014 // Next default values for new objects // diff --git a/cpp-lib/include/ISnaccRoseSessionSubscription.h b/cpp-lib/include/ISnaccRoseSessionSubscription.h new file mode 100644 index 0000000..cbfef73 --- /dev/null +++ b/cpp-lib/include/ISnaccRoseSessionSubscription.h @@ -0,0 +1,48 @@ +#ifndef _ISnaccRoseSessionSubscription_h_ +#define _ISnaccRoseSessionSubscription_h_ + +#include + +/* + * Per-session ROSE subscription contract for SnaccROSESender and SnaccROSEComponent. + * + * Server-side senders must override all methods. SnaccROSESender base implementations assert + * in debug builds when called without an override. + * + * When SnaccOperationBlockPolicy is BlockUnsupportedOperations and session state is marked, + * gluecode blocks outbound traffic via IsOperationBlocked. + */ +class ISnaccRoseSessionSubscription +{ +public: + virtual ~ISnaccRoseSessionSubscription() = default; + + /* Clears all subscribed events and supported invokes for this session (e.g. on disconnect). */ + virtual void ClearAllSubscriptions() = 0; + + /* Removes subscribed server-to-client event OPIDs for moduleIid only. */ + virtual void ClearSubscribedEvents(int moduleIid) = 0; + + /* Removes supported server-to-client invoke OPIDs for moduleIid only. */ + virtual void ClearSupportedInvokes(int moduleIid) = 0; + + /* Replaces the subscribed event OPID set for moduleIid and refreshes the flat dispatch lookup. */ + virtual void SetSubscribedEvents(int moduleIid, const std::list& eventOpIds) = 0; + + /* Adds one subscribed event OPID for moduleIid without replacing other module entries. */ + virtual void AddSubscribedEvent(int moduleIid, unsigned int uiEventOpId) = 0; + + /* Replaces the supported server-to-client invoke OPID set for moduleIid. */ + virtual void SetSupportedInvokes(int moduleIid, const std::list& invokeOpIds) = 0; + + /* Adds one supported invoke OPID for moduleIid without replacing other module entries. */ + virtual void AddSupportedInvoke(int moduleIid, unsigned int uiInvokeOpId) = 0; + + /* True when uiEventOpId is in the effective subscribed-event set for this session. */ + virtual bool IsSubscribedEvent(unsigned int uiEventOpId) const = 0; + + /* True when uiInvokeOpId is in the supported server-to-client invoke set for this session. */ + virtual bool IsSupportedInvoke(unsigned int uiInvokeOpId) const = 0; +}; + +#endif // _ISnaccRoseSessionSubscription_h_ diff --git a/cpp-lib/include/SnaccROSEBase.h b/cpp-lib/include/SnaccROSEBase.h index 2c83925..38937e8 100644 --- a/cpp-lib/include/SnaccROSEBase.h +++ b/cpp-lib/include/SnaccROSEBase.h @@ -238,15 +238,11 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Resolves ASN.1 module name owning the given operation id via the lookup table. */ const char* LookUpModuleName(unsigned int uiOpID) const; - /*! Configures whether outbound invokes are gated on a negotiate snapshot. Default Disabled. */ - void SetRemoteCapabilityMode(SnaccRemoteCapabilityMode mode); - SnaccRemoteCapabilityMode GetRemoteCapabilityMode() const; - /*! Stores the peer module snapshot from asnNegotiateInterface (or equivalent). */ - void ApplyRemoteModuleCapabilities(const SnaccLoadedModuleMap& remote); + void SetRemoteModuleCapabilities(const SnaccLoadedModuleMap& remote); void ClearRemoteModuleCapabilities(); - /*! True after ApplyRemoteModuleCapabilities() was called (even when the map is empty). */ + /*! True after SetRemoteModuleCapabilities() was called (even when the map is empty). */ bool HasRemoteModuleCapabilities() const; /*! True when the negotiate snapshot offers this invoke OPID. Debug ASSERT when !HasRemoteModuleCapabilities(). */ @@ -363,6 +359,12 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback */ virtual long HandleInvokeResult(long lRoseResult, const SNACC::ROSEMessage& responseMsg, SNACC::AsnType* pResult, SNACC::AsnType* pError, SnaccInvokeContext& ctx) override; + /** + * Records telemetry for an outbound operation stopped by invoke block policy. + * Does not send on the wire. @p lRoseResult is the value returned to the stub caller. + */ + void ReportOutboundBlocked(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, long lRoseResult, std::shared_ptr pCtx = {}); + /** * An event (invoke without result) that is send to the other side. Should only be called by the ROSE stub itself generated files * @@ -462,6 +464,15 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback /*! Returns true when the applied remote snapshot lists @p uiOpId as a supported invoke. */ bool InternalIsRemoteOperationSupported(unsigned int uiOpId) const; + bool OutboundBlockHasRemoteCapabilities() const override; + bool OutboundBlockIsRemoteOperationSupported(unsigned int uiOpId) const override; + + /*! Asserts via IsOperationBlocked, sets outRoseResult to ROSE_REJECT_REMOTENOTCAPABLE, records telemetry. Returns true when send must stop. */ + bool CompleteIfOperationBlocked(SNACC::ROSEInvoke* pInvoke, const char* szResolvedOperationName, bool bIsEvent, std::shared_ptr pCtx, long& outRoseResult); + + /*! When PauseRoseProcessing() is active: sets outRoseResult to ROSE_TE_SHUTDOWN, records telemetry. Returns true when send must stop. */ + bool CompleteIfProcessingShutdown(SNACC::ROSEInvoke* pInvoke, const char* szResolvedOperationName, std::chrono::steady_clock::time_point chronoCreated, std::shared_ptr pCtx, long& outRoseResult); + // The central process wide telemetry callback static inline SnaccTelemetryCallback* m_pTelemetryCallback{}; @@ -522,7 +533,6 @@ class SnaccROSEBase : public SnaccROSESender, public SnaccTelemetryCallback // Transport Encoding to be used SNACC::TransportEncoding m_eTransportEncoding{SNACC::TransportEncoding::UNKNOWN}; - SnaccRemoteCapabilityMode m_remoteCapabilityMode{SnaccRemoteCapabilityMode::Disabled}; SnaccLoadedModuleMap m_remoteModuleCapabilities; bool m_bRemoteModuleCapabilitiesSet{false}; diff --git a/cpp-lib/include/SnaccROSEInterfaces.h b/cpp-lib/include/SnaccROSEInterfaces.h index aef4ced..e19552d 100644 --- a/cpp-lib/include/SnaccROSEInterfaces.h +++ b/cpp-lib/include/SnaccROSEInterfaces.h @@ -1,6 +1,8 @@ #ifndef _SnaccROSEInterfaces_h_ #define _SnaccROSEInterfaces_h_ +#include "ISnaccRoseSessionSubscription.h" + #include #include #include @@ -240,9 +242,17 @@ class SnaccInvokeContext SNACC::AsnType* m_pAsyncError{}; }; -/*! SnaccROSESender is the interface that is used to dispatch inbound invokes and events - */ -class SnaccROSESender +/*! Controls whether outbound operations are blocked when absent from the peer/session capability snapshot. */ +enum class SnaccOperationBlockPolicy +{ + NeverBlock, + BlockUnsupportedOperations, +}; + +/*! SnaccROSESender is the transport-facing interface for generated ROSE stubs: send outbound + invokes and events, decode inbound invokes, and encode invoke responses. + Also implements ISnaccRoseSessionSubscription; base methods assert until overridden. */ +class SnaccROSESender : public virtual ISnaccRoseSessionSubscription { public: virtual std::shared_ptr CreateInvokeContext(const SnaccInvokeContextInit& init) = 0; @@ -336,6 +346,20 @@ class SnaccROSESender */ virtual long SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx = {}) = 0; + /*! Configures blocking of outbound operations not covered by session subscription or negotiate state. Default NeverBlock. */ + void SetOperationBlockPolicy(SnaccOperationBlockPolicy policy); + + SnaccOperationBlockPolicy GetOperationBlockPolicy() const; + + /*! True after MarkSessionSubscriptionStateSet() (typically when a subscribe handler updates session OPIDs). */ + bool HasSessionSubscriptionState() const; + + /*! Marks session subscription state as initialized; call from SetSubscribedEvents and related overrides. */ + void MarkSessionSubscriptionStateSet(); + + /*! True when BlockUnsupportedOperations is active, capability state is set, and the outbound op is not allowed. */ + bool IsOperationBlocked(unsigned int uiOpId, bool bIsEvent) const; + /* * Encodes a result as repsonse to an invoke * @@ -355,6 +379,34 @@ class SnaccROSESender * 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, const SNACC::AsnType* pError, std::string& strResponse, const wchar_t* szSessionID = nullptr) = 0; + + /* ISnaccRoseSessionSubscription — assert in debug when not overridden (server must implement). */ + void ClearAllSubscriptions() override; + void ClearSubscribedEvents(int moduleIid) override; + void ClearSupportedInvokes(int moduleIid) override; + void SetSubscribedEvents(int moduleIid, const std::list& eventOpIds) override; + void AddSubscribedEvent(int moduleIid, unsigned int uiEventOpId) override; + void AddSupportedInvoke(int moduleIid, unsigned int uiInvokeOpId) override; + void SetSupportedInvokes(int moduleIid, const std::list& invokeOpIds) override; + bool IsSubscribedEvent(unsigned int uiEventOpId) const override; + bool IsSupportedInvoke(unsigned int uiInvokeOpId) const override; + +protected: + /*! Override on client transports that apply asnNegotiateInterface snapshots. */ + virtual bool OutboundBlockHasRemoteCapabilities() const + { + return false; + } + + /*! Override on client transports; consulted when OutboundBlockHasRemoteCapabilities() is true. */ + virtual bool OutboundBlockIsRemoteOperationSupported(unsigned int uiOpId) const + { + (void)uiOpId; + return true; + } + + SnaccOperationBlockPolicy m_operationBlockPolicy{SnaccOperationBlockPolicy::NeverBlock}; + bool m_bSessionSubscriptionStateSet{false}; }; class SnaccScopedInvokeMessage @@ -382,11 +434,22 @@ class SnaccScopedInvokeMessage class SnaccROSEComponent { public: - SnaccROSEComponent(SnaccROSESender* pSB) + explicit SnaccROSEComponent(SnaccROSESender* pSB) + : m_pSB(pSB) { - m_pSB = pSB; } + /* Forwards ISnaccRoseSessionSubscription to m_pSB. */ + bool IsSubscribedEvent(unsigned int uiEventOpId) const; + bool IsSupportedInvoke(unsigned int uiInvokeOpId) const; + void ClearAllSubscriptions() const; + void ClearSubscribedEvents(int moduleIid) const; + void ClearSupportedInvokes(int moduleIid) const; + void SetSubscribedEvents(int moduleIid, const std::list& eventOpIds) const; + void AddSubscribedEvent(int moduleIid, unsigned int uiEventOpId) const; + void AddSupportedInvoke(int moduleIid, unsigned int uiInvokeOpId) const; + void SetSupportedInvokes(int moduleIid, const std::list& invokeOpIds) const; + protected: /*! Registers module version metadata on a startup lookup table (static RegisterOperations). */ static void RegisterModuleVersion(SnaccRoseOperationLookup& lookup, const char* szModuleName, const char* szModuleVersion); diff --git a/cpp-lib/include/SnaccRoseOperationLookup.h b/cpp-lib/include/SnaccRoseOperationLookup.h index 8ce062d..1dd4f6a 100644 --- a/cpp-lib/include/SnaccRoseOperationLookup.h +++ b/cpp-lib/include/SnaccRoseOperationLookup.h @@ -31,13 +31,6 @@ struct SnaccLoadedModuleInfo using SnaccLoadedModuleMap = std::map; -/*! Controls outbound invoke gating against a negotiate snapshot on SnaccROSEBase. */ -enum class SnaccRemoteCapabilityMode -{ - Disabled, - Enabled, -}; - /*! Immutable operation-id lookup table after Seal(). Fill at listener startup; share one instance across all connections on that listener. Lookup is read-only and needs no locking once sealed. */ diff --git a/cpp-lib/include/SnaccTelemetry.h b/cpp-lib/include/SnaccTelemetry.h index f09447e..2be1095 100644 --- a/cpp-lib/include/SnaccTelemetry.h +++ b/cpp-lib/include/SnaccTelemetry.h @@ -111,7 +111,9 @@ class SnaccTelemetryData // The lifecycle ended around an authentication reject. REJECT_AUTHENTICATION = 18, // Fallback value in case the exact failure reason could not be classified. - UNKNOWN_FAILURE = 19 + UNKNOWN_FAILURE = 19, + // Outbound send was skipped because invoke block policy rejected the operation. + OUTBOUND_BLOCKED = 20 }; // Returns a short debug-friendly text for the enum value. diff --git a/cpp-lib/src/SnaccROSEBase.cpp b/cpp-lib/src/SnaccROSEBase.cpp index 822743f..7f6bf32 100644 --- a/cpp-lib/src/SnaccROSEBase.cpp +++ b/cpp-lib/src/SnaccROSEBase.cpp @@ -573,6 +573,8 @@ SnaccTelemetryData::Reason GetUnhandledReasonFromResult(const long lRoseResult) return SnaccTelemetryData::Reason::TIMEOUT; case ROSE_TE_SHUTDOWN: return SnaccTelemetryData::Reason::SHUTDOWN; + case ROSE_REJECT_REMOTENOTCAPABLE: + return SnaccTelemetryData::Reason::OUTBOUND_BLOCKED; case ROSE_RE_DECODE_FAILED: return SnaccTelemetryData::Reason::DECODE_FAILED; case ROSE_RE_INVALID_ANSWER: @@ -648,17 +650,67 @@ const char* SnaccROSEBase::LookUpModuleName(unsigned int uiOpID) const return m_operationLookup.LookUpModuleName(uiOpID); } -void SnaccROSEBase::SetRemoteCapabilityMode(const SnaccRemoteCapabilityMode mode) +void SnaccROSESender::SetOperationBlockPolicy(const SnaccOperationBlockPolicy policy) { - m_remoteCapabilityMode = mode; + m_operationBlockPolicy = policy; } -SnaccRemoteCapabilityMode SnaccROSEBase::GetRemoteCapabilityMode() const +SnaccOperationBlockPolicy SnaccROSESender::GetOperationBlockPolicy() const { - return m_remoteCapabilityMode; + return m_operationBlockPolicy; } -void SnaccROSEBase::ApplyRemoteModuleCapabilities(const SnaccLoadedModuleMap& remote) +bool SnaccROSESender::HasSessionSubscriptionState() const +{ + return m_bSessionSubscriptionStateSet; +} + +void SnaccROSESender::MarkSessionSubscriptionStateSet() +{ + m_bSessionSubscriptionStateSet = true; +} + +bool SnaccROSESender::IsOperationBlocked(const unsigned int uiOpId, const bool bIsEvent) const +{ + if (m_operationBlockPolicy != SnaccOperationBlockPolicy::BlockUnsupportedOperations) + return false; + + if (bIsEvent) + { + if (!m_bSessionSubscriptionStateSet) + return false; + if (IsSubscribedEvent(uiOpId)) + return false; + ASSERT_FAILED("Outbound event blocked: operation id %u is not subscribed for this session", uiOpId); + return true; + } + + if (m_bSessionSubscriptionStateSet && !IsSupportedInvoke(uiOpId)) + { + ASSERT_FAILED("Outbound invoke blocked: operation id %u is not supported for this session", uiOpId); + return true; + } + + if (OutboundBlockHasRemoteCapabilities() && !OutboundBlockIsRemoteOperationSupported(uiOpId)) + { + ASSERT_FAILED("Outbound invoke blocked: operation id %u is not offered by the remote peer", uiOpId); + return true; + } + + return false; +} + +bool SnaccROSEBase::OutboundBlockHasRemoteCapabilities() const +{ + return m_bRemoteModuleCapabilitiesSet; +} + +bool SnaccROSEBase::OutboundBlockIsRemoteOperationSupported(const unsigned int uiOpId) const +{ + return InternalIsRemoteOperationSupported(uiOpId); +} + +void SnaccROSEBase::SetRemoteModuleCapabilities(const SnaccLoadedModuleMap& remote) { m_remoteModuleCapabilities = remote; m_bRemoteModuleCapabilitiesSet = true; @@ -690,7 +742,7 @@ bool SnaccROSEBase::InternalIsRemoteOperationSupported(const unsigned int uiOpId bool SnaccROSEBase::IsSupportedOperation(const unsigned int uiOpId) const { - ASSERT(m_bRemoteModuleCapabilitiesSet, "isSupportedOperation requires ApplyRemoteModuleCapabilities first"); + ASSERT(m_bRemoteModuleCapabilitiesSet, "isSupportedOperation requires SetRemoteModuleCapabilities first"); return InternalIsRemoteOperationSupported(uiOpId); } @@ -1607,6 +1659,40 @@ long SnaccROSEBase::Send(SNACC::ROSEInvoke* pInvoke, const char* szOperationName return lRoseResult; } +void SnaccROSEBase::ReportOutboundBlocked(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, const long lRoseResult, std::shared_ptr pCtx /*= {}*/) +{ + const auto chronoCreated = std::chrono::steady_clock::now(); + const char* szResolvedOperationName = ResolveOperationNameFromStub(szOperationName, pInvoke->operationID, this); + if (!pCtx) + pCtx = CreateInvokeContext(SnaccInvokeContextInit(SnaccInvokeDirection::OUTBOUND, pInvoke)); + + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); + telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::OUTBOUND_BLOCKED, lRoseResult, std::nullopt, std::move(pCtx)); + OnInvokeProcessed(telemetry); +} + +bool SnaccROSEBase::CompleteIfOperationBlocked(SNACC::ROSEInvoke* pInvoke, const char* szResolvedOperationName, const bool bIsEvent, std::shared_ptr pCtx, long& outRoseResult) +{ + if (!IsOperationBlocked(pInvoke->operationID.GetUInt(), bIsEvent)) + return false; + + outRoseResult = ROSE_REJECT_REMOTENOTCAPABLE; + ReportOutboundBlocked(pInvoke, szResolvedOperationName, outRoseResult, pCtx); + return true; +} + +bool SnaccROSEBase::CompleteIfProcessingShutdown(SNACC::ROSEInvoke* pInvoke, const char* szResolvedOperationName, const std::chrono::steady_clock::time_point chronoCreated, std::shared_ptr pCtx, long& outRoseResult) +{ + if (IsProcessingAllowed()) + return false; + + outRoseResult = ROSE_TE_SHUTDOWN; + auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); + telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::SHUTDOWN, outRoseResult, std::nullopt, std::move(pCtx)); + OnInvokeProcessed(telemetry); + return true; +} + long SnaccROSEBase::SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperationName, std::shared_ptr pCtx /*= {}*/) { const auto chronoCreated = std::chrono::steady_clock::now(); @@ -1616,7 +1702,15 @@ long SnaccROSEBase::SendEvent(SNACC::ROSEInvoke* pInvoke, const char* szOperatio auto& ctx = *pCtx; size_t stRequestData = 0; - const long lRoseResult = IsProcessingAllowed() ? Send(pInvoke, szResolvedOperationName, ctx, &stRequestData) : ROSE_TE_SHUTDOWN; + + long lRoseResult = ROSE_NOERROR; + if (CompleteIfProcessingShutdown(pInvoke, szResolvedOperationName, chronoCreated, pCtx, lRoseResult)) + return lRoseResult; + + if (CompleteIfOperationBlocked(pInvoke, szResolvedOperationName, true, pCtx, lRoseResult)) + return lRoseResult; + + lRoseResult = Send(pInvoke, szResolvedOperationName, ctx, &stRequestData); 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); @@ -1723,27 +1817,17 @@ long SnaccROSEBase::SendInvoke(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* pResu auto& ctx = *pCtx; const int iTimeout = ResolveInvokeTimeoutMs(ctx, m_lMaxInvokeWait); - if (!IsProcessingAllowed()) - { - 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; - } + long lRoseResult = ROSE_NOERROR; + if (CompleteIfProcessingShutdown(pInvoke, szResolvedOperationName, chronoCreated, pCtx, lRoseResult)) + return lRoseResult; - if (m_remoteCapabilityMode == SnaccRemoteCapabilityMode::Enabled && m_bRemoteModuleCapabilitiesSet && !InternalIsRemoteOperationSupported(pInvoke->operationID)) - { - ASSERT_FAILED("Outbound invoke blocked: operation %s (%u) is not offered by the remote peer", szResolvedOperationName ? szResolvedOperationName : "?", pInvoke->operationID.GetUInt()); - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); - telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::LOCAL_REJECT, ROSE_REJECT_REMOTENOTCAPABLE, std::nullopt, std::move(pCtx)); - OnInvokeProcessed(telemetry); - return ROSE_REJECT_REMOTENOTCAPABLE; - } + if (CompleteIfOperationBlocked(pInvoke, szResolvedOperationName, false, pCtx, lRoseResult)) + return lRoseResult; auto& pendingOP = AddPendingOperation(pInvoke->invokeID, pInvoke->operationID, szResolvedOperationName); size_t stRequestData = 0; - long lRoseResult = Send(pInvoke, szResolvedOperationName, ctx, &stRequestData); + lRoseResult = Send(pInvoke, szResolvedOperationName, ctx, &stRequestData); pendingOP.m_pTelemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, stRequestData, chronoCreated); if (lRoseResult == 0) @@ -1976,33 +2060,25 @@ long SnaccROSEBase::SendInvokeAsync(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* auto& ctx = *pCtx; - if (!IsProcessingAllowed()) + long lRoseResult = ROSE_NOERROR; + if (CompleteIfProcessingShutdown(pInvoke, szResolvedOperationName, chronoCreated, pCtx, lRoseResult)) { SnaccInvokeAsyncCallback shutdownCallback; if (!bFireAndForget && pCtx->HasAsyncCompletion()) shutdownCallback = pCtx->AsyncCallback(); - - 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, pCtx); - OnInvokeProcessed(telemetry); if (shutdownCallback) - shutdownCallback(ROSE_TE_SHUTDOWN, *pCtx); - return ROSE_TE_SHUTDOWN; + shutdownCallback(lRoseResult, *pCtx); + return lRoseResult; } - if (m_remoteCapabilityMode == SnaccRemoteCapabilityMode::Enabled && m_bRemoteModuleCapabilitiesSet && !InternalIsRemoteOperationSupported(pInvoke->operationID)) + if (CompleteIfOperationBlocked(pInvoke, szResolvedOperationName, false, pCtx, lRoseResult)) { - ASSERT_FAILED("Outbound invoke blocked: operation %s (%u) is not offered by the remote peer", szResolvedOperationName ? szResolvedOperationName : "?", pInvoke->operationID.GetUInt()); SnaccInvokeAsyncCallback rejectCallback; if (!bFireAndForget && pCtx->HasAsyncCompletion()) rejectCallback = pCtx->AsyncCallback(); - - auto telemetry = SnaccTelemetryData::Create(SnaccTelemetryData::Direction::OUTBOUND, pInvoke->operationID, szResolvedOperationName, 0, chronoCreated); - telemetry->finalize(SnaccTelemetryData::Outcome::UNHANDLED, SnaccTelemetryData::Stage::OUTBOUND_SEND, SnaccTelemetryData::Reason::LOCAL_REJECT, ROSE_REJECT_REMOTENOTCAPABLE, std::nullopt, pCtx); - OnInvokeProcessed(telemetry); if (rejectCallback) - rejectCallback(ROSE_REJECT_REMOTENOTCAPABLE, *pCtx); - return ROSE_REJECT_REMOTENOTCAPABLE; + rejectCallback(lRoseResult, *pCtx); + return lRoseResult; } auto& pendingOP = AddPendingOperation(pInvoke->invokeID, pInvoke->operationID, szResolvedOperationName); @@ -2023,7 +2099,7 @@ long SnaccROSEBase::SendInvokeAsync(SNACC::ROSEInvoke* pInvoke, SNACC::AsnType* } size_t stRequestData = 0; - const long lRoseResult = Send(pInvoke, szResolvedOperationName, ctx, &stRequestData); + lRoseResult = Send(pInvoke, szResolvedOperationName, ctx, &stRequestData); (void)stRequestData; if (bFireAndForget) diff --git a/cpp-lib/src/SnaccROSEInterfaces.cpp b/cpp-lib/src/SnaccROSEInterfaces.cpp new file mode 100644 index 0000000..907f553 --- /dev/null +++ b/cpp-lib/src/SnaccROSEInterfaces.cpp @@ -0,0 +1,122 @@ +#include "../include/SnaccROSEInterfaces.h" +#include "snacc-assert.h" + +void SnaccROSESender::ClearAllSubscriptions() +{ + ASSERT_FAILED("ClearAllSubscriptions not implemented on this SnaccROSESender"); +} + +void SnaccROSESender::ClearSubscribedEvents(int moduleIid) +{ + (void)moduleIid; + ASSERT_FAILED("ClearSubscribedEvents not implemented on this SnaccROSESender"); +} + +void SnaccROSESender::ClearSupportedInvokes(int moduleIid) +{ + (void)moduleIid; + ASSERT_FAILED("ClearSupportedInvokes not implemented on this SnaccROSESender"); +} + +void SnaccROSESender::SetSubscribedEvents(int moduleIid, const std::list& eventOpIds) +{ + (void)moduleIid; + (void)eventOpIds; + ASSERT_FAILED("SetSubscribedEvents not implemented on this SnaccROSESender"); +} + +void SnaccROSESender::AddSubscribedEvent(int moduleIid, unsigned int uiEventOpId) +{ + (void)moduleIid; + (void)uiEventOpId; + ASSERT_FAILED("AddSubscribedEvent not implemented on this SnaccROSESender"); +} + +void SnaccROSESender::AddSupportedInvoke(int moduleIid, unsigned int uiInvokeOpId) +{ + (void)moduleIid; + (void)uiInvokeOpId; + ASSERT_FAILED("AddSupportedInvoke not implemented on this SnaccROSESender"); +} + +void SnaccROSESender::SetSupportedInvokes(int moduleIid, const std::list& invokeOpIds) +{ + (void)moduleIid; + (void)invokeOpIds; + ASSERT_FAILED("SetSupportedInvokes not implemented on this SnaccROSESender"); +} + +bool SnaccROSESender::IsSubscribedEvent(unsigned int uiEventOpId) const +{ + (void)uiEventOpId; + ASSERT_FAILED("IsSubscribedEvent not implemented on this SnaccROSESender"); + return false; +} + +bool SnaccROSESender::IsSupportedInvoke(unsigned int uiInvokeOpId) const +{ + (void)uiInvokeOpId; + ASSERT_FAILED("IsSupportedInvoke not implemented on this SnaccROSESender"); + return false; +} + +bool SnaccROSEComponent::IsSubscribedEvent(unsigned int uiEventOpId) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + return m_pSB ? m_pSB->IsSubscribedEvent(uiEventOpId) : false; +} + +bool SnaccROSEComponent::IsSupportedInvoke(unsigned int uiInvokeOpId) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + return m_pSB ? m_pSB->IsSupportedInvoke(uiInvokeOpId) : false; +} + +void SnaccROSEComponent::ClearAllSubscriptions() const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->ClearAllSubscriptions(); +} + +void SnaccROSEComponent::ClearSubscribedEvents(int moduleIid) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->ClearSubscribedEvents(moduleIid); +} + +void SnaccROSEComponent::ClearSupportedInvokes(int moduleIid) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->ClearSupportedInvokes(moduleIid); +} + +void SnaccROSEComponent::SetSubscribedEvents(int moduleIid, const std::list& eventOpIds) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->SetSubscribedEvents(moduleIid, eventOpIds); +} + +void SnaccROSEComponent::AddSubscribedEvent(int moduleIid, unsigned int uiEventOpId) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->AddSubscribedEvent(moduleIid, uiEventOpId); +} + +void SnaccROSEComponent::AddSupportedInvoke(int moduleIid, unsigned int uiInvokeOpId) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->AddSupportedInvoke(moduleIid, uiInvokeOpId); +} + +void SnaccROSEComponent::SetSupportedInvokes(int moduleIid, const std::list& invokeOpIds) const +{ + ASSERT(m_pSB, "SnaccROSEComponent has no SnaccROSESender"); + if (m_pSB) + m_pSB->SetSupportedInvokes(moduleIid, invokeOpIds); +} diff --git a/cpp-lib/src/SnaccTelemetry.cpp b/cpp-lib/src/SnaccTelemetry.cpp index f1923c1..497c3ca 100644 --- a/cpp-lib/src/SnaccTelemetry.cpp +++ b/cpp-lib/src/SnaccTelemetry.cpp @@ -104,6 +104,8 @@ const char* SnaccTelemetryData::GetDebugText(Reason reason) return "REJECT_AUTHENTICATION"; case Reason::UNKNOWN_FAILURE: return "UNKNOWN_FAILURE"; + case Reason::OUTBOUND_BLOCKED: + return "OUTBOUND_BLOCKED"; default: ASSERT(0); return "INVALID_REASON"; diff --git a/cpp-lib/tests/CMakeLists.txt b/cpp-lib/tests/CMakeLists.txt index 620e7d4..a5c060e 100644 --- a/cpp-lib/tests/CMakeLists.txt +++ b/cpp-lib/tests/CMakeLists.txt @@ -58,7 +58,9 @@ add_executable(cpp-lib-sample-runtime-tests logical_failure_tests.cpp module_registry_tests.cpp public_api_tests.cpp - remote_capability_tests.cpp + client_invoke_block_policy_tests.cpp + rose_session_subscription_tests.cpp + server_invoke_block_policy_tests.cpp telemetry_tests.cpp transport_failure_tests.cpp lifecycle_tests.cpp diff --git a/cpp-lib/tests/remote_capability_tests.cpp b/cpp-lib/tests/client_invoke_block_policy_tests.cpp similarity index 68% rename from cpp-lib/tests/remote_capability_tests.cpp rename to cpp-lib/tests/client_invoke_block_policy_tests.cpp index fc7d64e..d8734b9 100644 --- a/cpp-lib/tests/remote_capability_tests.cpp +++ b/cpp-lib/tests/client_invoke_block_policy_tests.cpp @@ -43,7 +43,7 @@ SnaccLoadedModuleMap BuildRemoteSnapshotWithoutGetSettings() } } // namespace -class RemoteCapabilityRuntimeTest : public RuntimeTestBase +class ClientInvokeBlockPolicyRuntimeTest : public RuntimeTestBase { protected: void InitializeConnectedEndpoints() @@ -85,54 +85,54 @@ TEST(RemoteCapabilityModuleHelperTest, BuildRemoteModuleCapabilitiesPopulatesInv EXPECT_NE(module.m_events.end(), module.m_events.find(4150u)); } -TEST_F(RemoteCapabilityRuntimeTest, EnabledWithoutSnapshotDoesNotGateInvoke) +TEST_F(ClientInvokeBlockPolicyRuntimeTest, BlockUnsupportedOperationsWithoutSnapshotDoesNotBlockInvoke) { InitializeConnectedEndpoints(); - m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + m_client.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } -TEST_F(RemoteCapabilityRuntimeTest, EnabledWithUnsupportedOpIdReturnsRemoteNotCapable) +TEST_F(ClientInvokeBlockPolicyRuntimeTest, BlockUnsupportedOperationsWithUnsupportedOpIdReturnsRemoteNotCapable) { InitializeConnectedEndpoints(); - m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); - m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + m_client.SetRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); + m_client.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_REJECT_REMOTENOTCAPABLE, roseResult); EXPECT_EQ(0u, ServerInboundObservation().TransportSendCount()); } -TEST_F(RemoteCapabilityRuntimeTest, EnabledWithSupportedOpIdSendsInvoke) +TEST_F(ClientInvokeBlockPolicyRuntimeTest, BlockUnsupportedOperationsWithSupportedOpIdSendsInvoke) { InitializeConnectedEndpoints(); - m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithGetSettingsOnly()); - m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + m_client.SetRemoteModuleCapabilities(BuildRemoteSnapshotWithGetSettingsOnly()); + m_client.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } -TEST_F(RemoteCapabilityRuntimeTest, DisabledWithSnapshotDoesNotGateInvoke) +TEST_F(ClientInvokeBlockPolicyRuntimeTest, NeverBlockWithSnapshotDoesNotBlockInvoke) { InitializeConnectedEndpoints(); - m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); - m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Disabled); + m_client.SetRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); + m_client.SetOperationBlockPolicy(SnaccOperationBlockPolicy::NeverBlock); const long roseResult = InvokeGetSettingsOnClient(); EXPECT_EQ(ROSE_NOERROR, roseResult); EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } -TEST_F(RemoteCapabilityRuntimeTest, ClearRemoteCapabilitiesStopsGating) +TEST_F(ClientInvokeBlockPolicyRuntimeTest, ClearRemoteCapabilitiesStopsBlocking) { InitializeConnectedEndpoints(); - m_client.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); - m_client.SetRemoteCapabilityMode(SnaccRemoteCapabilityMode::Enabled); + m_client.SetRemoteModuleCapabilities(BuildRemoteSnapshotWithoutGetSettings()); + m_client.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); m_client.ClearRemoteModuleCapabilities(); const long roseResult = InvokeGetSettingsOnClient(); @@ -140,13 +140,13 @@ TEST_F(RemoteCapabilityRuntimeTest, ClearRemoteCapabilitiesStopsGating) EXPECT_GE(ClientOutboundTransportObservation().TransportSendCount(), 1u); } -TEST_F(RemoteCapabilityRuntimeTest, IsSupportedOperationReflectsAppliedSnapshot) +TEST_F(ClientInvokeBlockPolicyRuntimeTest, IsSupportedOperationReflectsAppliedSnapshot) { SnaccRoseOperationLookup lookup; - RuntimeEndpoint endpoint{L"RemoteCapabilityQuery", "remote-capability-query", lookup}; + RuntimeEndpoint endpoint{L"ClientInvokeBlockPolicyQuery", "client-invoke-block-policy-query", lookup}; ENetUC_Settings_ManagerROSE::RegisterOperations(lookup); - endpoint.ApplyRemoteModuleCapabilities(BuildRemoteSnapshotWithGetSettingsOnly()); + endpoint.SetRemoteModuleCapabilities(BuildRemoteSnapshotWithGetSettingsOnly()); EXPECT_TRUE(endpoint.HasRemoteModuleCapabilities()); EXPECT_TRUE(endpoint.IsSupportedOperation(4100u)); EXPECT_FALSE(endpoint.IsSupportedOperation(4101u)); diff --git a/cpp-lib/tests/rose_session_subscription_tests.cpp b/cpp-lib/tests/rose_session_subscription_tests.cpp new file mode 100644 index 0000000..2bc09f9 --- /dev/null +++ b/cpp-lib/tests/rose_session_subscription_tests.cpp @@ -0,0 +1,106 @@ +#include + +#include + +namespace +{ +class RecordingRoseSender : public SnaccROSESender +{ +public: + std::shared_ptr CreateInvokeContext(const SnaccInvokeContextInit& init) override + { + (void)init; + return {}; + } + + long GetNextInvokeID() override + { + return 1; + } + + SNACC::EAsnLogLevel GetLogLevel(const bool /*bOutbound*/) override + { + return SNACC::EAsnLogLevel::DISABLED; + } + + bool LogTransportData(const bool /*bOutbound*/, const SNACC::TransportEncoding /*encoding*/, const char* /*szOperationName*/, const char* /*szData*/, const size_t /*size*/, const SNACC::ROSEMessage* /*pMsg*/, const SJson::Value* /*pParsedValue*/) override + { + return false; + } + + long SendInvoke(SNACC::ROSEInvoke* /*pInvoke*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/, const char* /*szOperationName*/, std::shared_ptr /*pCtx*/) override + { + return ROSE_NOERROR; + } + + long SendInvokeAsync(SNACC::ROSEInvoke* /*pInvoke*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/, const char* /*szOperationName*/, std::shared_ptr /*pCtx*/) override + { + return ROSE_NOERROR; + } + + long HandleInvokeResult(long /*lRoseResult*/, const SNACC::ROSEMessage& /*responseMsg*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/, SnaccInvokeContext& /*ctx*/) override + { + return ROSE_NOERROR; + } + + long HandleOnInvokeResult(SNACC::InvokeResult /*invokeResult*/, const SNACC::ROSEInvoke& /*invoke*/, SnaccInvokeContext& /*ctx*/, std::string& /*strResponse*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/) override + { + return ROSE_NOERROR; + } + + long DecodeInvoke(const SNACC::ROSEMessage& /*invokeMessage*/, SNACC::AsnType* /*pArgument*/) override + { + return ROSE_NOERROR; + } + + long SendEvent(SNACC::ROSEInvoke* /*pInvoke*/, const char* /*szOperationName*/, std::shared_ptr /*pCtx*/) override + { + return ROSE_NOERROR; + } + + long EncodeResult(unsigned int /*uiInvokeID*/, const SNACC::AsnType* /*pResult*/, std::string& /*strResponse*/, const wchar_t* /*szSessionID*/) override + { + return ROSE_NOERROR; + } + + long EncodeError(unsigned int /*uiInvokeID*/, const SNACC::AsnType* /*pError*/, std::string& /*strResponse*/, const wchar_t* /*szSessionID*/) override + { + return ROSE_NOERROR; + } + + bool IsSubscribedEvent(unsigned int uiEventOpId) const override + { + return uiEventOpId == subscribedEventOpId; + } + + void AddSubscribedEvent(int moduleIid, unsigned int uiEventOpId) override + { + lastModuleIid = moduleIid; + subscribedEventOpId = uiEventOpId; + } + + int lastModuleIid = 0; + unsigned int subscribedEventOpId = 0; +}; + +class TestRoseComponent : public SnaccROSEComponent +{ +public: + explicit TestRoseComponent(SnaccROSESender* pSender) + : SnaccROSEComponent(pSender) + { + } +}; +} // namespace + +TEST(RoseSessionSubscription, SnaccROSEComponentForwardsToSender) +{ + RecordingRoseSender sender; + TestRoseComponent component(&sender); + + EXPECT_FALSE(component.IsSubscribedEvent(2109)); + component.AddSubscribedEvent(2104, 2109); + EXPECT_EQ(2104, sender.lastModuleIid); + EXPECT_TRUE(component.IsSubscribedEvent(2109)); + EXPECT_FALSE(component.IsSubscribedEvent(2170)); +} diff --git a/cpp-lib/tests/server_invoke_block_policy_tests.cpp b/cpp-lib/tests/server_invoke_block_policy_tests.cpp new file mode 100644 index 0000000..8e09ed1 --- /dev/null +++ b/cpp-lib/tests/server_invoke_block_policy_tests.cpp @@ -0,0 +1,121 @@ +#include + +#include + +namespace +{ +class RecordingSubscriptionSender : public SnaccROSESender +{ +public: + unsigned int subscribedEventOpId = 0; + unsigned int supportedInvokeOpId = 0; + + std::shared_ptr CreateInvokeContext(const SnaccInvokeContextInit& init) override + { + (void)init; + return {}; + } + + long GetNextInvokeID() override + { + return 1; + } + + SNACC::EAsnLogLevel GetLogLevel(const bool /*bOutbound*/) override + { + return SNACC::EAsnLogLevel::DISABLED; + } + + bool LogTransportData(const bool /*bOutbound*/, const SNACC::TransportEncoding /*encoding*/, const char* /*szOperationName*/, const char* /*szData*/, const size_t /*size*/, const SNACC::ROSEMessage* /*pMsg*/, const SJson::Value* /*pParsedValue*/) override + { + return false; + } + + long SendInvoke(SNACC::ROSEInvoke* /*pInvoke*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/, const char* /*szOperationName*/, std::shared_ptr /*pCtx*/) override + { + return ROSE_NOERROR; + } + + long SendInvokeAsync(SNACC::ROSEInvoke* /*pInvoke*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/, const char* /*szOperationName*/, std::shared_ptr /*pCtx*/) override + { + return ROSE_NOERROR; + } + + long HandleInvokeResult(long /*lRoseResult*/, const SNACC::ROSEMessage& /*responseMsg*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/, SnaccInvokeContext& /*ctx*/) override + { + return ROSE_NOERROR; + } + + long HandleOnInvokeResult(SNACC::InvokeResult /*invokeResult*/, const SNACC::ROSEInvoke& /*invoke*/, SnaccInvokeContext& /*ctx*/, std::string& /*strResponse*/, SNACC::AsnType* /*pResult*/, SNACC::AsnType* /*pError*/) override + { + return ROSE_NOERROR; + } + + long DecodeInvoke(const SNACC::ROSEMessage& /*invokeMessage*/, SNACC::AsnType* /*pArgument*/) override + { + return ROSE_NOERROR; + } + + long SendEvent(SNACC::ROSEInvoke* /*pInvoke*/, const char* /*szOperationName*/, std::shared_ptr /*pCtx*/) override + { + return ROSE_NOERROR; + } + + long EncodeResult(unsigned int /*uiInvokeID*/, const SNACC::AsnType* /*pResult*/, std::string& /*strResponse*/, const wchar_t* /*szSessionID*/) override + { + return ROSE_NOERROR; + } + + long EncodeError(unsigned int /*uiInvokeID*/, const SNACC::AsnType* /*pError*/, std::string& /*strResponse*/, const wchar_t* /*szSessionID*/) override + { + return ROSE_NOERROR; + } + + bool IsSubscribedEvent(unsigned int uiEventOpId) const override + { + return uiEventOpId == subscribedEventOpId; + } + + bool IsSupportedInvoke(unsigned int uiInvokeOpId) const override + { + return uiInvokeOpId == supportedInvokeOpId; + } +}; + +TEST(ServerInvokeBlockPolicyTest, BlockWithoutStateDoesNotBlock) +{ + RecordingSubscriptionSender sender; + sender.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); + EXPECT_FALSE(sender.IsOperationBlocked(2109, true)); + EXPECT_FALSE(sender.IsOperationBlocked(2109, false)); +} + +TEST(ServerInvokeBlockPolicyTest, BlockWithStateBlocksUnsubscribedEvent) +{ + RecordingSubscriptionSender sender; + sender.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); + sender.MarkSessionSubscriptionStateSet(); + sender.subscribedEventOpId = 2170; + EXPECT_TRUE(sender.IsOperationBlocked(2109, true)); + EXPECT_FALSE(sender.IsOperationBlocked(2170, true)); +} + +TEST(ServerInvokeBlockPolicyTest, BlockWithStateBlocksUnsupportedInvoke) +{ + RecordingSubscriptionSender sender; + sender.SetOperationBlockPolicy(SnaccOperationBlockPolicy::BlockUnsupportedOperations); + sender.MarkSessionSubscriptionStateSet(); + sender.supportedInvokeOpId = 3001; + EXPECT_TRUE(sender.IsOperationBlocked(3002, false)); + EXPECT_FALSE(sender.IsOperationBlocked(3001, false)); +} + +TEST(ServerInvokeBlockPolicyTest, NeverBlockDoesNotBlock) +{ + RecordingSubscriptionSender sender; + sender.SetOperationBlockPolicy(SnaccOperationBlockPolicy::NeverBlock); + sender.MarkSessionSubscriptionStateSet(); + EXPECT_FALSE(sender.IsOperationBlocked(2109, true)); + EXPECT_FALSE(sender.IsOperationBlocked(2109, false)); +} +} // namespace diff --git a/docs/build.md b/docs/build.md index ebaa800..69454f7 100644 --- a/docs/build.md +++ b/docs/build.md @@ -37,7 +37,7 @@ Environment variables: `SNACCLIB7_ROOT`, `SNACC_CMAKE_BUILD_DIR`, `SNACC_CMAKE_G `samples/prepare.bat`, `samples/prepare.sh`, and `ROSE/makesnaccrose.bat` call these helpers before running Node or invoking the compiler directly. -TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + remote capability). The run scripts assemble an ephemeral `tests/workdir/` copy (gluecode plus sample `ENetUC_Common` stubs) so the source `gluecode/` tree stays compiler-only. Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. +TypeScript gluecode unit tests live under `compiler/back-ends/ts-gen/tests/` (registry + client invoke block policy). The run scripts assemble an ephemeral `tests/workdir/` copy (gluecode plus sample `ENetUC_Common` stubs) so the source `gluecode/` tree stays compiler-only. Run `scripts/run_gluecode_tests.bat` (Windows) or `scripts/run_gluecode_tests.sh` (Linux) after `samples/prepare` has installed node-client dependencies. ### CMake variables diff --git a/scripts/run_gluecode_tests.bat b/scripts/run_gluecode_tests.bat index eee9e14..35d558c 100644 --- a/scripts/run_gluecode_tests.bat +++ b/scripts/run_gluecode_tests.bat @@ -25,10 +25,13 @@ copy /Y "%STUB_DIR%\ENetUC_Common_Converter.ts" "%WORKDIR%\" >nul for %%T in ( TSASN1Base.registry.test.ts - TSASN1Base.remoteCapability.test.ts + TSASN1Base.invokeBlockPolicy.test.ts + TSASN1Base.roseSessionSubscription.test.ts + TSASN1Base.pauseRoseProcessing.test.ts TSModuleCapabilities.test.ts ) do ( echo Running %%T ... + set "NODE_PATH=%NODE_MODULES%" npx --yes tsx "%TEST_DIR%\%%T" if errorlevel 1 set "EXIT_CODE=1" ) diff --git a/scripts/run_gluecode_tests.sh b/scripts/run_gluecode_tests.sh index e7a5402..b6fefe7 100644 --- a/scripts/run_gluecode_tests.sh +++ b/scripts/run_gluecode_tests.sh @@ -27,7 +27,9 @@ cp "$STUB_DIR/ENetUC_Common.ts" "$STUB_DIR/ENetUC_Common_Converter.ts" "$WORKDIR for test_file in \ TSASN1Base.registry.test.ts \ - TSASN1Base.remoteCapability.test.ts \ + TSASN1Base.invokeBlockPolicy.test.ts \ + TSASN1Base.roseSessionSubscription.test.ts \ + TSASN1Base.pauseRoseProcessing.test.ts \ TSModuleCapabilities.test.ts do echo "Running $test_file ..." diff --git a/version.h b/version.h index 17c5f86..cc87dfc 100644 --- a/version.h +++ b/version.h @@ -1,8 +1,8 @@ #ifndef VERSION_H #define VERSION_H -#define VERSION "7.0.15" -#define VERSION_RC 7, 0, 15 -#define RELDATE "20.08.2026" +#define VERSION "7.0.16" +#define VERSION_RC 7, 0, 16 +#define RELDATE "26.08.2026" #endif // VERSION_H