From 72450e4d655ecb27120993b77b28ad6cf7298469 Mon Sep 17 00:00:00 2001 From: Andrei Litvin Date: Thu, 6 Jun 2024 11:48:57 -0400 Subject: [PATCH] Make `CommandHandler` a pure interface class, have actual implementation in `CommandHandlerImpl` (#33736) * First pass: renames only and moved things around. It should not yet compile * Move CommandHandler::Callback to CommandHandlerImpl::Callback * Prepare some compile fixes. Uses of preparation of responses is not ok yet * More replace fixes * More compile updates * Try to implement the addResponse properly * Many more updates for compilation * More compile tests - getting closer * Things compile * Restyle * Split out CommandHandler (the interface) from CommandHandlerImpl (actual IM implementation) * Restyle * make GetExchangeContext also be virtual * Fix some includes that were previously part of CommandHandler.h * Restyle * No need for casts and private APIs: Exchange context is actually available * Code review feedback * Restyle * Upgrading docs * Slight example fix --------- Co-authored-by: Andrei Litvin --- docs/index.md | 1 + docs/upgrading.md | 62 + .../lighting-app/tizen/src/DBusInterface.cpp | 15 +- src/app/BUILD.gn | 2 + src/app/CommandHandler.cpp | 1009 +--------------- src/app/CommandHandler.h | 617 ++-------- src/app/CommandHandlerImpl.cpp | 1057 +++++++++++++++++ src/app/CommandHandlerImpl.h | 476 ++++++++ src/app/CommandResponseSender.cpp | 6 +- src/app/CommandResponseSender.h | 16 +- src/app/InteractionModelEngine.cpp | 24 +- src/app/InteractionModelEngine.h | 8 +- src/app/tests/TestCommandInteraction.cpp | 73 +- .../java/OTAProviderDelegateBridge.cpp | 1 + 14 files changed, 1764 insertions(+), 1603 deletions(-) create mode 100644 docs/upgrading.md create mode 100644 src/app/CommandHandlerImpl.cpp create mode 100644 src/app/CommandHandlerImpl.h diff --git a/docs/index.md b/docs/index.md index b7615a476818df..1f8e806dc4cce9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,6 +22,7 @@ BUG_REPORT code_generation zap_clusters spec_clusters +upgrading ERROR_CODES ``` diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 00000000000000..cd18b533484279 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,62 @@ +# Upgrading notes + +## API changes and code migration + +### `CommandHandler` + +`CommandHandler` ability to directly invoke `Prepare/TLV-Write/Finish` cycles +has been changed to only expose `AddResponse/AddStatus/AddClusterSpecific*`. + +Original versions of `CommandHandler` exposed the following low-level +implementation-specific methods: `PrepareCommand`, +`PrepareInvokeResponseCommand`, `GetCommandDataIBTLVWriter` and `FinishCommand`. +These are not exposed anymore and instead one should use `AddResponse` or +`AddResponseData`. When using an `EncodableToTLV` argument, the same +functionality should be achievable. + +Example + +Before: + +```cpp + +const CommandHandler::InvokeResponseParameters prepareParams(requestPath); +ReturnOnFailure(commandHandler->PrepareInvokeResponseCommand(path, prepareParams)); + +TLV::TLVWriter *writer = commandHandler->GetCommandDataIBTLVWriter(); +ReturnOnFailure(writer->Put(chip::TLV::ContextTag(1), 123)); +ReturnOnFailure(writer->Put(chip::TLV::ContextTag(2), 234)); +return commandHandler->FinishCommand(); +``` + +After: + +```cpp + +class ReplyEncoder : public DataModel::EncodableToTLV +{ +public: + CHIP_ERROR EncodeTo(TLV::TLVWriter & writer, TLV::Tag tag) const override + { + TLV::TLVType outerType; + ReturnErrorOnFailure(writer.StartContainer(tag, TLV::kTLVType_Structure, outerType)); + + ReturnOnFailure(writer.Put(chip::TLV::ContextTag(1), 123)); + ReturnOnFailure(writer.Put(chip::TLV::ContextTag(2), 234)); + + return writer.EndContainer(outerType); + } +}; + +// ... +ReplyEncoder replyEncoder; +commandHandler->AddResponse(path, kReplyCommandId, replyEncoder); + +// or if error handling is implemented: +// +// ReturnErrorOnFailure(commandHandler->AddResponseData(path, kReplyCommandId, replyEncoder)); +// +// In many cases error recovery from not being able to send a reply is not easy or expected, +// so code does AddResponse rather than AddResponseData. + +``` diff --git a/examples/lighting-app/tizen/src/DBusInterface.cpp b/examples/lighting-app/tizen/src/DBusInterface.cpp index e39ca5c1e1232c..50dbd37930b47f 100644 --- a/examples/lighting-app/tizen/src/DBusInterface.cpp +++ b/examples/lighting-app/tizen/src/DBusInterface.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -36,13 +37,13 @@ using namespace chip::app; namespace example { -// Dummy class to satisfy the CommandHandler::Callback interface. -class CommandHandlerCallback : public CommandHandler::Callback +// Dummy class to satisfy the CommandHandlerImpl::Callback interface. +class CommandHandlerImplCallback : public CommandHandlerImpl::Callback { public: using Status = Protocols::InteractionModel::Status; - void OnDone(CommandHandler & apCommandObj) {} - void DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) {} + void OnDone(CommandHandlerImpl & apCommandObj) {} + void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) {} Status CommandExists(const ConcreteCommandPath & aCommandPath) { return Status::Success; } }; @@ -188,8 +189,10 @@ gboolean DBusInterface::OnColorTemperatureChanged(LightAppColorControl * colorCo // Do not handle on-change event if it was triggered by internal set VerifyOrReturnValue(!self->mInternalSet, G_DBUS_METHOD_INVOCATION_HANDLED); - CommandHandlerCallback callback; - CommandHandler handler(&callback); + // TODO: creating such a complex object seems odd here + // as handler seems not used to send back any response back anywhere. + CommandHandlerImplCallback callback; + CommandHandlerImpl handler(&callback); ConcreteCommandPath path{ self->mEndpointId, Clusters::ColorControl::Id, 0 }; diff --git a/src/app/BUILD.gn b/src/app/BUILD.gn index f49a40f398842a..55bc5f305ddd99 100644 --- a/src/app/BUILD.gn +++ b/src/app/BUILD.gn @@ -338,6 +338,8 @@ source_set("command-handler") { "CommandHandler.cpp", "CommandHandler.h", "CommandHandlerExchangeInterface.h", + "CommandHandlerImpl.cpp", + "CommandHandlerImpl.h", ] public_deps = [ diff --git a/src/app/CommandHandler.cpp b/src/app/CommandHandler.cpp index 309685491a8d82..90ebdaa43603d7 100644 --- a/src/app/CommandHandler.cpp +++ b/src/app/CommandHandler.cpp @@ -1,6 +1,6 @@ /* * - * Copyright (c) 2020 Project CHIP Authors + * Copyright (c) 2020-2024 Project CHIP Authors * All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,802 +17,8 @@ */ #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - namespace chip { namespace app { -using Status = Protocols::InteractionModel::Status; - -CommandHandler::CommandHandler(Callback * apCallback) : mpCallback(apCallback), mSuppressResponse(false) {} - -CommandHandler::CommandHandler(TestOnlyOverrides & aTestOverride, Callback * apCallback) : CommandHandler(apCallback) -{ - if (aTestOverride.commandPathRegistry) - { - mMaxPathsPerInvoke = aTestOverride.commandPathRegistry->MaxSize(); - mCommandPathRegistry = aTestOverride.commandPathRegistry; - } - if (aTestOverride.commandResponder) - { - SetExchangeInterface(aTestOverride.commandResponder); - } -} - -CommandHandler::~CommandHandler() -{ - InvalidateHandles(); -} - -CHIP_ERROR CommandHandler::AllocateBuffer() -{ - // We should only allocate a buffer if we will be sending out a response. - VerifyOrReturnError(ResponsesAccepted(), CHIP_ERROR_INCORRECT_STATE); - - if (!mBufferAllocated) - { - mCommandMessageWriter.Reset(); - - System::PacketBufferHandle commandPacket = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes); - VerifyOrReturnError(!commandPacket.IsNull(), CHIP_ERROR_NO_MEMORY); - - mCommandMessageWriter.Init(std::move(commandPacket)); - ReturnErrorOnFailure(mInvokeResponseBuilder.InitWithEndBufferReserved(&mCommandMessageWriter)); - - if (mReserveSpaceForMoreChunkMessages) - { - ReturnErrorOnFailure(mInvokeResponseBuilder.ReserveSpaceForMoreChunkedMessages()); - } - - // Sending an InvokeResponse to an InvokeResponse is going to be removed from the spec soon. - // It was never implemented in the SDK, and there are no command responses that expect a - // command response. This means we will never receive an InvokeResponse Message in response - // to an InvokeResponse Message that we are sending. This means that the only response - // we are expecting to receive in response to an InvokeResponse Message that we are - // sending-out is a status when we are chunking multiple responses. As a result, to satisfy the - // condition that we don't set SuppressResponse to true while also setting - // MoreChunkedMessages to true, we are hardcoding the value to false here. - mInvokeResponseBuilder.SuppressResponse(/* aSuppressResponse = */ false); - ReturnErrorOnFailure(mInvokeResponseBuilder.GetError()); - - mInvokeResponseBuilder.CreateInvokeResponses(/* aReserveEndBuffer = */ true); - ReturnErrorOnFailure(mInvokeResponseBuilder.GetError()); - - mBufferAllocated = true; - MoveToState(State::NewResponseMessage); - } - - return CHIP_NO_ERROR; -} - -Status CommandHandler::OnInvokeCommandRequest(CommandHandlerExchangeInterface & commandResponder, - System::PacketBufferHandle && payload, bool isTimedInvoke) -{ - VerifyOrDieWithMsg(mState == State::Idle, DataManagement, "state should be Idle"); - - SetExchangeInterface(&commandResponder); - - // Using RAII here: if this is the only handle remaining, DecrementHoldOff will - // call the CommandHandler::OnDone callback when this function returns. - Handle workHandle(this); - - Status status = ProcessInvokeRequest(std::move(payload), isTimedInvoke); - mGoneAsync = true; - return status; -} - -CHIP_ERROR CommandHandler::TryAddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - DataModel::EncodableToTLV & aEncodable) -{ - ConcreteCommandPath responseCommandPath = { aRequestCommandPath.mEndpointId, aRequestCommandPath.mClusterId, - aResponseCommandId }; - - InvokeResponseParameters prepareParams(aRequestCommandPath); - prepareParams.SetStartOrEndDataStruct(false); - - { - ScopedChange internalCallToAddResponse(mInternalCallToAddResponseData, true); - ReturnErrorOnFailure(PrepareInvokeResponseCommand(responseCommandPath, prepareParams)); - } - - TLV::TLVWriter * writer = GetCommandDataIBTLVWriter(); - VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE); - ReturnErrorOnFailure(aEncodable.EncodeTo(*writer, TLV::ContextTag(CommandDataIB::Tag::kFields))); - return FinishCommand(/* aEndDataStruct = */ false); -} - -CHIP_ERROR CommandHandler::ValidateInvokeRequestMessageAndBuildRegistry(InvokeRequestMessage::Parser & invokeRequestMessage) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - size_t commandCount = 0; - bool commandRefExpected = false; - InvokeRequests::Parser invokeRequests; - - ReturnErrorOnFailure(invokeRequestMessage.GetInvokeRequests(&invokeRequests)); - TLV::TLVReader invokeRequestsReader; - invokeRequests.GetReader(&invokeRequestsReader); - - ReturnErrorOnFailure(TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */)); - - // If this is a GroupRequest the only thing to check is that there is only one - // CommandDataIB. - if (IsGroupRequest()) - { - VerifyOrReturnError(commandCount == 1, CHIP_ERROR_INVALID_ARGUMENT); - return CHIP_NO_ERROR; - } - // While technically any commandCount == 1 should already be unique and does not need - // any further validation, we do need to read and populate the registry to help - // in building the InvokeResponse. - - VerifyOrReturnError(commandCount <= MaxPathsPerInvoke(), CHIP_ERROR_INVALID_ARGUMENT); - - // If there is more than one CommandDataIB, spec states that CommandRef must be provided. - commandRefExpected = commandCount > 1; - - while (CHIP_NO_ERROR == (err = invokeRequestsReader.Next())) - { - VerifyOrReturnError(TLV::AnonymousTag() == invokeRequestsReader.GetTag(), CHIP_ERROR_INVALID_ARGUMENT); - CommandDataIB::Parser commandData; - ReturnErrorOnFailure(commandData.Init(invokeRequestsReader)); - - // First validate that we can get a ConcreteCommandPath. - CommandPathIB::Parser commandPath; - ConcreteCommandPath concretePath(0, 0, 0); - ReturnErrorOnFailure(commandData.GetPath(&commandPath)); - ReturnErrorOnFailure(commandPath.GetConcreteCommandPath(concretePath)); - - // Grab the CommandRef if there is one, and validate that it's there when it - // has to be. - std::optional commandRef; - uint16_t ref; - err = commandData.GetRef(&ref); - VerifyOrReturnError(err == CHIP_NO_ERROR || err == CHIP_END_OF_TLV, err); - if (err == CHIP_END_OF_TLV && commandRefExpected) - { - return CHIP_ERROR_INVALID_ARGUMENT; - } - if (err == CHIP_NO_ERROR) - { - commandRef.emplace(ref); - } - - // Adding can fail if concretePath is not unique, or if commandRef is a value - // and is not unique, or if we have already added more paths than we support. - ReturnErrorOnFailure(GetCommandPathRegistry().Add(concretePath, commandRef)); - } - - // It's OK/expected to have reached the end of the container without failure. - if (CHIP_END_OF_TLV == err) - { - err = CHIP_NO_ERROR; - } - ReturnErrorOnFailure(err); - return invokeRequestMessage.ExitContainer(); -} - -Status CommandHandler::ProcessInvokeRequest(System::PacketBufferHandle && payload, bool isTimedInvoke) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - System::PacketBufferTLVReader reader; - InvokeRequestMessage::Parser invokeRequestMessage; - InvokeRequests::Parser invokeRequests; - reader.Init(std::move(payload)); - VerifyOrReturnError(invokeRequestMessage.Init(reader) == CHIP_NO_ERROR, Status::InvalidAction); -#if CHIP_CONFIG_IM_PRETTY_PRINT - invokeRequestMessage.PrettyPrint(); -#endif - VerifyOrDie(mpResponder); - if (mpResponder->GetGroupId().HasValue()) - { - SetGroupRequest(true); - } - - // When updating this code, please remember to make corresponding changes to TestOnlyInvokeCommandRequestWithFaultsInjected. - VerifyOrReturnError(invokeRequestMessage.GetSuppressResponse(&mSuppressResponse) == CHIP_NO_ERROR, Status::InvalidAction); - VerifyOrReturnError(invokeRequestMessage.GetTimedRequest(&mTimedRequest) == CHIP_NO_ERROR, Status::InvalidAction); - VerifyOrReturnError(invokeRequestMessage.GetInvokeRequests(&invokeRequests) == CHIP_NO_ERROR, Status::InvalidAction); - VerifyOrReturnError(mTimedRequest == isTimedInvoke, Status::TimedRequestMismatch); - - { - InvokeRequestMessage::Parser validationInvokeRequestMessage = invokeRequestMessage; - VerifyOrReturnError(ValidateInvokeRequestMessageAndBuildRegistry(validationInvokeRequestMessage) == CHIP_NO_ERROR, - Status::InvalidAction); - } - - TLV::TLVReader invokeRequestsReader; - invokeRequests.GetReader(&invokeRequestsReader); - - size_t commandCount = 0; - VerifyOrReturnError(TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */) == CHIP_NO_ERROR, - Status::InvalidAction); - if (commandCount > 1) - { - mReserveSpaceForMoreChunkMessages = true; - } - - while (CHIP_NO_ERROR == (err = invokeRequestsReader.Next())) - { - VerifyOrReturnError(TLV::AnonymousTag() == invokeRequestsReader.GetTag(), Status::InvalidAction); - CommandDataIB::Parser commandData; - VerifyOrReturnError(commandData.Init(invokeRequestsReader) == CHIP_NO_ERROR, Status::InvalidAction); - Status status = Status::Success; - if (IsGroupRequest()) - { - status = ProcessGroupCommandDataIB(commandData); - } - else - { - status = ProcessCommandDataIB(commandData); - } - if (status != Status::Success) - { - return status; - } - } - - // if we have exhausted this container - if (CHIP_END_OF_TLV == err) - { - err = CHIP_NO_ERROR; - } - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); - VerifyOrReturnError(invokeRequestMessage.ExitContainer() == CHIP_NO_ERROR, Status::InvalidAction); - return Status::Success; -} - -void CommandHandler::Close() -{ - mSuppressResponse = false; - mpResponder = nullptr; - MoveToState(State::AwaitingDestruction); - - // We must finish all async work before we can shut down a CommandHandler. The actual CommandHandler MUST finish their work - // in reasonable time or there is a bug. The only case for releasing CommandHandler without CommandHandler::Handle releasing its - // reference is the stack shutting down, in which case Close() is not called. So the below check should always pass. - VerifyOrDieWithMsg(mPendingWork == 0, DataManagement, "CommandHandler::Close() called with %u unfinished async work items", - static_cast(mPendingWork)); - InvalidateHandles(); - - if (mpCallback) - { - mpCallback->OnDone(*this); - } -} - -void CommandHandler::AddToHandleList(Handle * apHandle) -{ - mpHandleList.PushBack(apHandle); -} - -void CommandHandler::RemoveFromHandleList(Handle * apHandle) -{ - VerifyOrDie(mpHandleList.Contains(apHandle)); - mpHandleList.Remove(apHandle); -} - -void CommandHandler::InvalidateHandles() -{ - for (auto handle = mpHandleList.begin(); handle != mpHandleList.end(); ++handle) - { - handle->Invalidate(); - } -} - -void CommandHandler::IncrementHoldOff(Handle * apHandle) -{ - mPendingWork++; - AddToHandleList(apHandle); -} - -void CommandHandler::DecrementHoldOff(Handle * apHandle) -{ - - mPendingWork--; - ChipLogDetail(DataManagement, "Decreasing reference count for CommandHandler, remaining %u", - static_cast(mPendingWork)); - - RemoveFromHandleList(apHandle); - - if (mPendingWork != 0) - { - return; - } - - if (mpResponder == nullptr) - { - ChipLogProgress(DataManagement, "Skipping command response: response sender is null"); - } - else if (!IsGroupRequest()) - { - CHIP_ERROR err = FinalizeLastInvokeResponseMessage(); - if (err != CHIP_NO_ERROR) - { - ChipLogError(DataManagement, "Failed to finalize command response: %" CHIP_ERROR_FORMAT, err.Format()); - } - } - - Close(); -} - -namespace { -// We use this when the sender did not actually provide a CommandFields struct, -// to avoid downstream consumers having to worry about cases when there is or is -// not a struct available. We use an empty struct with anonymous tag, since we -// can't use a context tag at top level, and consumers should not care about the -// tag here). -constexpr uint8_t sNoFields[] = { - CHIP_TLV_STRUCTURE(CHIP_TLV_TAG_ANONYMOUS), - CHIP_TLV_END_OF_CONTAINER, -}; -} // anonymous namespace - -Status CommandHandler::ProcessCommandDataIB(CommandDataIB::Parser & aCommandElement) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - CommandPathIB::Parser commandPath; - ConcreteCommandPath concretePath(0, 0, 0); - TLV::TLVReader commandDataReader; - - // NOTE: errors may occur before the concrete command path is even fully decoded. - - err = aCommandElement.GetPath(&commandPath); - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); - - err = commandPath.GetConcreteCommandPath(concretePath); - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); - - { - Status commandExists = mpCallback->CommandExists(concretePath); - if (commandExists != Status::Success) - { - ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x", - ChipLogValueMEI(concretePath.mCommandId), ChipLogValueMEI(concretePath.mClusterId), - concretePath.mEndpointId); - return FallibleAddStatus(concretePath, commandExists) != CHIP_NO_ERROR ? Status::Failure : Status::Success; - } - } - - { - Access::SubjectDescriptor subjectDescriptor = GetSubjectDescriptor(); - Access::RequestPath requestPath{ .cluster = concretePath.mClusterId, .endpoint = concretePath.mEndpointId }; - Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath); - err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege); - if (err != CHIP_NO_ERROR) - { - if (err != CHIP_ERROR_ACCESS_DENIED) - { - return FallibleAddStatus(concretePath, Status::Failure) != CHIP_NO_ERROR ? Status::Failure : Status::Success; - } - // TODO: when wildcard invokes are supported, handle them to discard rather than fail with status - return FallibleAddStatus(concretePath, Status::UnsupportedAccess) != CHIP_NO_ERROR ? Status::Failure : Status::Success; - } - } - - if (CommandNeedsTimedInvoke(concretePath.mClusterId, concretePath.mCommandId) && !IsTimedInvoke()) - { - // TODO: when wildcard invokes are supported, discard a - // wildcard-expanded path instead of returning a status. - return FallibleAddStatus(concretePath, Status::NeedsTimedInteraction) != CHIP_NO_ERROR ? Status::Failure : Status::Success; - } - - if (CommandIsFabricScoped(concretePath.mClusterId, concretePath.mCommandId)) - { - // SPEC: Else if the command in the path is fabric-scoped and there is no accessing fabric, - // a CommandStatusIB SHALL be generated with the UNSUPPORTED_ACCESS Status Code. - - // Fabric-scoped commands are not allowed before a specific accessing fabric is available. - // This is mostly just during a PASE session before AddNOC. - if (GetAccessingFabricIndex() == kUndefinedFabricIndex) - { - // TODO: when wildcard invokes are supported, discard a - // wildcard-expanded path instead of returning a status. - return FallibleAddStatus(concretePath, Status::UnsupportedAccess) != CHIP_NO_ERROR ? Status::Failure : Status::Success; - } - } - - err = aCommandElement.GetFields(&commandDataReader); - if (CHIP_END_OF_TLV == err) - { - ChipLogDetail(DataManagement, - "Received command without data for Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, - concretePath.mEndpointId, ChipLogValueMEI(concretePath.mClusterId), ChipLogValueMEI(concretePath.mCommandId)); - commandDataReader.Init(sNoFields); - err = commandDataReader.Next(); - } - if (CHIP_NO_ERROR == err) - { - ChipLogDetail(DataManagement, "Received command for Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, - concretePath.mEndpointId, ChipLogValueMEI(concretePath.mClusterId), ChipLogValueMEI(concretePath.mCommandId)); - SuccessOrExit(err = DataModelCallbacks::GetInstance()->PreCommandReceived(concretePath, GetSubjectDescriptor())); - mpCallback->DispatchCommand(*this, concretePath, commandDataReader); - DataModelCallbacks::GetInstance()->PostCommandReceived(concretePath, GetSubjectDescriptor()); - } - -exit: - if (err != CHIP_NO_ERROR) - { - return FallibleAddStatus(concretePath, Status::InvalidCommand) != CHIP_NO_ERROR ? Status::Failure : Status::Success; - } - - // We have handled the error status above and put the error status in response, now return success status so we can process - // other commands in the invoke request. - return Status::Success; -} - -Status CommandHandler::ProcessGroupCommandDataIB(CommandDataIB::Parser & aCommandElement) -{ - CHIP_ERROR err = CHIP_NO_ERROR; - CommandPathIB::Parser commandPath; - TLV::TLVReader commandDataReader; - ClusterId clusterId; - CommandId commandId; - GroupId groupId; - FabricIndex fabric; - - Credentials::GroupDataProvider::GroupEndpoint mapping; - Credentials::GroupDataProvider * groupDataProvider = Credentials::GetGroupDataProvider(); - Credentials::GroupDataProvider::EndpointIterator * iterator; - - err = aCommandElement.GetPath(&commandPath); - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); - - err = commandPath.GetGroupCommandPath(&clusterId, &commandId); - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); - - VerifyOrDie(mpResponder); - // The optionalGroupId must have a value, otherwise we wouldn't have reached this code path. - groupId = mpResponder->GetGroupId().Value(); - fabric = GetAccessingFabricIndex(); - - ChipLogDetail(DataManagement, "Received group command for Group=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, - groupId, ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId)); - - err = aCommandElement.GetFields(&commandDataReader); - if (CHIP_END_OF_TLV == err) - { - ChipLogDetail(DataManagement, - "Received command without data for Group=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, groupId, - ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId)); - commandDataReader.Init(sNoFields); - err = commandDataReader.Next(); - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); - } - VerifyOrReturnError(err == CHIP_NO_ERROR, Status::Failure); - - // Per spec, we do the "is this a timed command?" check for every path, but - // since all paths that fail it just get silently discarded we can do it - // once up front and discard all the paths at once. Ordering with respect - // to ACL and command presence checks does not matter, because the behavior - // is the same for all of them: ignore the path. - if (CommandNeedsTimedInvoke(clusterId, commandId)) - { - // Group commands are never timed. - return Status::Success; - } - - // No check for `CommandIsFabricScoped` unlike in `ProcessCommandDataIB()` since group commands - // always have an accessing fabric, by definition. - - // Find which endpoints can process the command, and dispatch to them. - iterator = groupDataProvider->IterateEndpoints(fabric); - VerifyOrReturnError(iterator != nullptr, Status::Failure); - - while (iterator->Next(mapping)) - { - if (groupId != mapping.group_id) - { - continue; - } - - ChipLogDetail(DataManagement, - "Processing group command for Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, - mapping.endpoint_id, ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId)); - - const ConcreteCommandPath concretePath(mapping.endpoint_id, clusterId, commandId); - - if (mpCallback->CommandExists(concretePath) != Status::Success) - { - ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x", - ChipLogValueMEI(commandId), ChipLogValueMEI(clusterId), mapping.endpoint_id); - - continue; - } - - { - Access::SubjectDescriptor subjectDescriptor = GetSubjectDescriptor(); - Access::RequestPath requestPath{ .cluster = concretePath.mClusterId, .endpoint = concretePath.mEndpointId }; - Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath); - err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege); - if (err != CHIP_NO_ERROR) - { - // NOTE: an expected error is CHIP_ERROR_ACCESS_DENIED, but there could be other unexpected errors; - // therefore, keep processing subsequent commands, and if any errors continue, those subsequent - // commands will likewise fail. - continue; - } - } - if ((err = DataModelCallbacks::GetInstance()->PreCommandReceived(concretePath, GetSubjectDescriptor())) == CHIP_NO_ERROR) - { - TLV::TLVReader dataReader(commandDataReader); - mpCallback->DispatchCommand(*this, concretePath, dataReader); - DataModelCallbacks::GetInstance()->PostCommandReceived(concretePath, GetSubjectDescriptor()); - } - else - { - ChipLogError(DataManagement, - "Error when calling PreCommandReceived for Endpoint=%u Cluster=" ChipLogFormatMEI - " Command=" ChipLogFormatMEI " : %" CHIP_ERROR_FORMAT, - mapping.endpoint_id, ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId), err.Format()); - continue; - } - } - iterator->Release(); - return Status::Success; -} - -CHIP_ERROR CommandHandler::TryAddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus) -{ - // Return early when response should not be sent out. - VerifyOrReturnValue(ResponsesAccepted(), CHIP_NO_ERROR); - - ReturnErrorOnFailure(PrepareStatus(aCommandPath)); - CommandStatusIB::Builder & commandStatus = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus(); - StatusIB::Builder & statusIBBuilder = commandStatus.CreateErrorStatus(); - ReturnErrorOnFailure(commandStatus.GetError()); - statusIBBuilder.EncodeStatusIB(aStatus); - ReturnErrorOnFailure(statusIBBuilder.GetError()); - return FinishStatus(); -} - -CHIP_ERROR CommandHandler::AddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus) -{ - return TryAddingResponse([&]() -> CHIP_ERROR { return TryAddStatusInternal(aCommandPath, aStatus); }); -} - -void CommandHandler::AddStatus(const ConcreteCommandPath & aCommandPath, const Protocols::InteractionModel::Status aStatus, - const char * context) -{ - - CHIP_ERROR error = FallibleAddStatus(aCommandPath, aStatus, context); - - if (error != CHIP_NO_ERROR) - { - ChipLogError(DataManagement, "Failed to add command status: %" CHIP_ERROR_FORMAT, error.Format()); - // TODO(#30453) we could call mpResponder->ResponseDropped() if err == CHIP_ERROR_NO_MEMORY. This should - // be done as a follow up so that change can be evaluated as a standalone PR. - - // Do not crash if the status has not been added due to running out of packet buffers or other resources. - // It is better to drop a single response than to go offline and lose all sessions and subscriptions. - VerifyOrDie(error == CHIP_ERROR_NO_MEMORY); - } -} - -CHIP_ERROR CommandHandler::FallibleAddStatus(const ConcreteCommandPath & path, const Protocols::InteractionModel::Status status, - const char * context) -{ - if (status != Status::Success) - { - if (context == nullptr) - { - context = "no additional context"; - } - - ChipLogError(DataManagement, - "Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI " status " ChipLogFormatIMStatus " (%s)", - path.mEndpointId, ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mCommandId), - ChipLogValueIMStatus(status), context); - } - - return AddStatusInternal(path, StatusIB(status)); -} - -CHIP_ERROR CommandHandler::AddClusterSpecificSuccess(const ConcreteCommandPath & aCommandPath, ClusterStatus aClusterStatus) -{ - return AddStatusInternal(aCommandPath, StatusIB(Status::Success, aClusterStatus)); -} - -CHIP_ERROR CommandHandler::AddClusterSpecificFailure(const ConcreteCommandPath & aCommandPath, ClusterStatus aClusterStatus) -{ - return AddStatusInternal(aCommandPath, StatusIB(Status::Failure, aClusterStatus)); -} - -CHIP_ERROR CommandHandler::PrepareInvokeResponseCommand(const ConcreteCommandPath & aResponseCommandPath, - const CommandHandler::InvokeResponseParameters & aPrepareParameters) -{ - auto commandPathRegistryEntry = GetCommandPathRegistry().Find(aPrepareParameters.mRequestCommandPath); - VerifyOrReturnValue(commandPathRegistryEntry.has_value(), CHIP_ERROR_INCORRECT_STATE); - - return PrepareInvokeResponseCommand(*commandPathRegistryEntry, aResponseCommandPath, aPrepareParameters.mStartOrEndDataStruct); -} - -CHIP_ERROR CommandHandler::PrepareCommand(const ConcreteCommandPath & aResponseCommandPath, bool aStartDataStruct) -{ - // Legacy code is calling the deprecated version of PrepareCommand. If we are in a case where - // there was a single command in the request, we can just assume this response is triggered by - // the single command. - size_t countOfPathRegistryEntries = GetCommandPathRegistry().Count(); - - // At this point application supports Batch Invoke Commands since CommandPathRegistry has more than 1 entry, - // but application is calling the deprecated PrepareCommand. We have no way to determine the associated CommandRef - // to put into the InvokeResponse. - VerifyOrDieWithMsg(countOfPathRegistryEntries == 1, DataManagement, - "Seemingly device supports batch commands, but is calling the deprecated PrepareCommand API"); - - auto commandPathRegistryEntry = GetCommandPathRegistry().GetFirstEntry(); - VerifyOrReturnValue(commandPathRegistryEntry.has_value(), CHIP_ERROR_INCORRECT_STATE); - - return PrepareInvokeResponseCommand(*commandPathRegistryEntry, aResponseCommandPath, aStartDataStruct); -} - -CHIP_ERROR CommandHandler::PrepareInvokeResponseCommand(const CommandPathRegistryEntry & apCommandPathRegistryEntry, - const ConcreteCommandPath & aCommandPath, bool aStartDataStruct) -{ - // Intentionally omitting the ResponsesAccepted early exit. Direct use of PrepareInvokeResponseCommand - // is discouraged, as it often indicates incorrect usage patterns (see GitHub issue #32486). - // If you're encountering CHIP_ERROR_INCORRECT_STATE, refactoring to use AddResponse is recommended. - ReturnErrorOnFailure(AllocateBuffer()); - - if (!mInternalCallToAddResponseData && mState == State::AddedCommand) - { - // An attempt is being made to add CommandData InvokeResponse using primitive - // CommandHandler APIs. While not recommended, as this potentially leaves the - // CommandHandler in an incorrect state upon failure, this approach is permitted - // for legacy reasons. To maximize the likelihood of success, particularly when - // handling large amounts of data, we try to obtain a new, completely empty - // InvokeResponseMessage, as the existing one already has space occupied. - ReturnErrorOnFailure(FinalizeInvokeResponseMessageAndPrepareNext()); - } - - CreateBackupForResponseRollback(); - // - // We must not be in the middle of preparing a command, or having prepared or sent one. - // - VerifyOrReturnError(mState == State::NewResponseMessage || mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE); - - // TODO(#30453): See if we can pass this back up the stack so caller can provide this instead of taking up - // space in CommandHanlder. - mRefForResponse = apCommandPathRegistryEntry.ref; - - MoveToState(State::Preparing); - InvokeResponseIBs::Builder & invokeResponses = mInvokeResponseBuilder.GetInvokeResponses(); - InvokeResponseIB::Builder & invokeResponse = invokeResponses.CreateInvokeResponse(); - ReturnErrorOnFailure(invokeResponses.GetError()); - - CommandDataIB::Builder & commandData = invokeResponse.CreateCommand(); - ReturnErrorOnFailure(commandData.GetError()); - CommandPathIB::Builder & path = commandData.CreatePath(); - ReturnErrorOnFailure(commandData.GetError()); - ReturnErrorOnFailure(path.Encode(aCommandPath)); - if (aStartDataStruct) - { - ReturnErrorOnFailure(commandData.GetWriter()->StartContainer(TLV::ContextTag(CommandDataIB::Tag::kFields), - TLV::kTLVType_Structure, mDataElementContainerType)); - } - MoveToState(State::AddingCommand); - return CHIP_NO_ERROR; -} - -CHIP_ERROR CommandHandler::FinishCommand(bool aStartDataStruct) -{ - // Intentionally omitting the ResponsesAccepted early exit. Direct use of FinishCommand - // is discouraged, as it often indicates incorrect usage patterns (see GitHub issue #32486). - // If you're encountering CHIP_ERROR_INCORRECT_STATE, refactoring to use AddResponse is recommended. - VerifyOrReturnError(mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE); - CommandDataIB::Builder & commandData = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetCommand(); - if (aStartDataStruct) - { - ReturnErrorOnFailure(commandData.GetWriter()->EndContainer(mDataElementContainerType)); - } - - if (mRefForResponse.has_value()) - { - ReturnErrorOnFailure(commandData.Ref(*mRefForResponse)); - } - - ReturnErrorOnFailure(commandData.EndOfCommandDataIB()); - ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().EndOfInvokeResponseIB()); - MoveToState(State::AddedCommand); - return CHIP_NO_ERROR; -} - -CHIP_ERROR CommandHandler::PrepareStatus(const ConcreteCommandPath & aCommandPath) -{ - ReturnErrorOnFailure(AllocateBuffer()); - // - // We must not be in the middle of preparing a command, or having prepared or sent one. - // - VerifyOrReturnError(mState == State::NewResponseMessage || mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE); - if (mState == State::AddedCommand) - { - CreateBackupForResponseRollback(); - } - - auto commandPathRegistryEntry = GetCommandPathRegistry().Find(aCommandPath); - VerifyOrReturnError(commandPathRegistryEntry.has_value(), CHIP_ERROR_INCORRECT_STATE); - mRefForResponse = commandPathRegistryEntry->ref; - - MoveToState(State::Preparing); - InvokeResponseIBs::Builder & invokeResponses = mInvokeResponseBuilder.GetInvokeResponses(); - InvokeResponseIB::Builder & invokeResponse = invokeResponses.CreateInvokeResponse(); - ReturnErrorOnFailure(invokeResponses.GetError()); - CommandStatusIB::Builder & commandStatus = invokeResponse.CreateStatus(); - ReturnErrorOnFailure(commandStatus.GetError()); - CommandPathIB::Builder & path = commandStatus.CreatePath(); - ReturnErrorOnFailure(commandStatus.GetError()); - ReturnErrorOnFailure(path.Encode(aCommandPath)); - MoveToState(State::AddingCommand); - return CHIP_NO_ERROR; -} - -CHIP_ERROR CommandHandler::FinishStatus() -{ - VerifyOrReturnError(mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE); - - CommandStatusIB::Builder & commandStatus = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus(); - if (mRefForResponse.has_value()) - { - ReturnErrorOnFailure(commandStatus.Ref(*mRefForResponse)); - } - - ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus().EndOfCommandStatusIB()); - ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().EndOfInvokeResponseIB()); - MoveToState(State::AddedCommand); - return CHIP_NO_ERROR; -} - -void CommandHandler::CreateBackupForResponseRollback() -{ - VerifyOrReturn(mState == State::NewResponseMessage || mState == State::AddedCommand); - VerifyOrReturn(mInvokeResponseBuilder.GetInvokeResponses().GetError() == CHIP_NO_ERROR); - VerifyOrReturn(mInvokeResponseBuilder.GetError() == CHIP_NO_ERROR); - mInvokeResponseBuilder.Checkpoint(mBackupWriter); - mBackupState = mState; - mRollbackBackupValid = true; -} - -CHIP_ERROR CommandHandler::RollbackResponse() -{ - VerifyOrReturnError(mRollbackBackupValid, CHIP_ERROR_INCORRECT_STATE); - VerifyOrReturnError(mState == State::Preparing || mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE); - ChipLogDetail(DataManagement, "Rolling back response"); - // TODO(#30453): Rollback of mInvokeResponseBuilder should handle resetting - // InvokeResponses. - mInvokeResponseBuilder.GetInvokeResponses().ResetError(); - mInvokeResponseBuilder.Rollback(mBackupWriter); - MoveToState(mBackupState); - mRollbackBackupValid = false; - return CHIP_NO_ERROR; -} - -TLV::TLVWriter * CommandHandler::GetCommandDataIBTLVWriter() -{ - if (mState != State::AddingCommand) - { - return nullptr; - } - - return mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetCommand().GetWriter(); -} - -FabricIndex CommandHandler::GetAccessingFabricIndex() const -{ - VerifyOrDie(!mGoneAsync); - VerifyOrDie(mpResponder); - return mpResponder->GetAccessingFabricIndex(); -} void CommandHandler::Handle::Init(CommandHandler * handler) { @@ -825,7 +31,7 @@ void CommandHandler::Handle::Init(CommandHandler * handler) CommandHandler * CommandHandler::Handle::Get() { - // Not safe to work with CommandHandler in parallel with other Matter work. + // Not safe to work with CommandHandlerImpl in parallel with other Matter work. assertChipStackLockedByCurrentThread(); return mpHandler; @@ -845,216 +51,5 @@ CommandHandler::Handle::Handle(CommandHandler * handler) Init(handler); } -CHIP_ERROR CommandHandler::FinalizeInvokeResponseMessageAndPrepareNext() -{ - ReturnErrorOnFailure(FinalizeInvokeResponseMessage(/* aHasMoreChunks = */ true)); - // After successfully finalizing InvokeResponseMessage, no buffer should remain - // allocated. - VerifyOrDie(!mBufferAllocated); - CHIP_ERROR err = AllocateBuffer(); - if (err != CHIP_NO_ERROR) - { - // TODO(#30453): Improve ResponseDropped calls to occur only when dropping is - // definitively guaranteed. - // Response dropping is not yet definitive as a subsequent call - // to AllocateBuffer might succeed. - VerifyOrDie(mpResponder); - mpResponder->ResponseDropped(); - } - return err; -} - -CHIP_ERROR CommandHandler::FinalizeInvokeResponseMessage(bool aHasMoreChunks) -{ - System::PacketBufferHandle packet; - - VerifyOrReturnError(mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE); - ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().EndOfInvokeResponses()); - if (aHasMoreChunks) - { - // Unreserving space previously reserved for MoreChunkedMessages is done - // in the call to mInvokeResponseBuilder.MoreChunkedMessages. - mInvokeResponseBuilder.MoreChunkedMessages(aHasMoreChunks); - ReturnErrorOnFailure(mInvokeResponseBuilder.GetError()); - } - ReturnErrorOnFailure(mInvokeResponseBuilder.EndOfInvokeResponseMessage()); - ReturnErrorOnFailure(mCommandMessageWriter.Finalize(&packet)); - VerifyOrDie(mpResponder); - mpResponder->AddInvokeResponseToSend(std::move(packet)); - mBufferAllocated = false; - mRollbackBackupValid = false; - return CHIP_NO_ERROR; -} - -void CommandHandler::SetExchangeInterface(CommandHandlerExchangeInterface * commandResponder) -{ - VerifyOrDieWithMsg(mState == State::Idle, DataManagement, "CommandResponseSender can only be set in idle state"); - mpResponder = commandResponder; -} - -const char * CommandHandler::GetStateStr() const -{ -#if CHIP_DETAIL_LOGGING - switch (mState) - { - case State::Idle: - return "Idle"; - - case State::NewResponseMessage: - return "NewResponseMessage"; - - case State::Preparing: - return "Preparing"; - - case State::AddingCommand: - return "AddingCommand"; - - case State::AddedCommand: - return "AddedCommand"; - - case State::DispatchResponses: - return "DispatchResponses"; - - case State::AwaitingDestruction: - return "AwaitingDestruction"; - } -#endif // CHIP_DETAIL_LOGGING - return "N/A"; -} - -void CommandHandler::MoveToState(const State aTargetState) -{ - mState = aTargetState; - ChipLogDetail(DataManagement, "Command handler moving to [%10.10s]", GetStateStr()); -} - -#if CHIP_WITH_NLFAULTINJECTION - -namespace { - -CHIP_ERROR TestOnlyExtractCommandPathFromNextInvokeRequest(TLV::TLVReader & invokeRequestsReader, - ConcreteCommandPath & concretePath) -{ - ReturnErrorOnFailure(invokeRequestsReader.Next(TLV::AnonymousTag())); - CommandDataIB::Parser commandData; - ReturnErrorOnFailure(commandData.Init(invokeRequestsReader)); - CommandPathIB::Parser commandPath; - ReturnErrorOnFailure(commandData.GetPath(&commandPath)); - return commandPath.GetConcreteCommandPath(concretePath); -} - -[[maybe_unused]] const char * GetFaultInjectionTypeStr(CommandHandler::NlFaultInjectionType faultType) -{ - switch (faultType) - { - case CommandHandler::NlFaultInjectionType::SeparateResponseMessages: - return "Each response will be sent in a separate InvokeResponseMessage. The order of responses will be the same as the " - "original request."; - case CommandHandler::NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder: - return "Each response will be sent in a separate InvokeResponseMessage. The order of responses will be reversed from the " - "original request."; - case CommandHandler::NlFaultInjectionType::SkipSecondResponse: - return "Single InvokeResponseMessages. Dropping response to second request"; - } - ChipLogError(DataManagement, "TH Failure: Unexpected fault type"); - chipAbort(); -} - -} // anonymous namespace - -// This method intentionally duplicates code from other sections. While code consolidation -// is generally preferred, here we prioritize generating a clear crash message to aid in -// troubleshooting test failures. -void CommandHandler::TestOnlyInvokeCommandRequestWithFaultsInjected(CommandHandlerExchangeInterface & commandResponder, - System::PacketBufferHandle && payload, bool isTimedInvoke, - NlFaultInjectionType faultType) -{ - VerifyOrDieWithMsg(mState == State::Idle, DataManagement, "TH Failure: state should be Idle, issue with TH"); - SetExchangeInterface(&commandResponder); - - ChipLogProgress(DataManagement, "Response to InvokeRequestMessage overridden by fault injection"); - ChipLogProgress(DataManagement, " Injecting the following response:%s", GetFaultInjectionTypeStr(faultType)); - - Handle workHandle(this); - VerifyOrDieWithMsg(!commandResponder.GetGroupId().HasValue(), DataManagement, "DUT Failure: Unexpected Group Command"); - - System::PacketBufferTLVReader reader; - InvokeRequestMessage::Parser invokeRequestMessage; - InvokeRequests::Parser invokeRequests; - reader.Init(std::move(payload)); - VerifyOrDieWithMsg(invokeRequestMessage.Init(reader) == CHIP_NO_ERROR, DataManagement, - "TH Failure: Failed 'invokeRequestMessage.Init(reader)'"); -#if CHIP_CONFIG_IM_PRETTY_PRINT - invokeRequestMessage.PrettyPrint(); -#endif - - VerifyOrDieWithMsg(invokeRequestMessage.GetSuppressResponse(&mSuppressResponse) == CHIP_NO_ERROR, DataManagement, - "DUT Failure: Mandatory SuppressResponse field missing"); - VerifyOrDieWithMsg(invokeRequestMessage.GetTimedRequest(&mTimedRequest) == CHIP_NO_ERROR, DataManagement, - "DUT Failure: Mandatory TimedRequest field missing"); - VerifyOrDieWithMsg(invokeRequestMessage.GetInvokeRequests(&invokeRequests) == CHIP_NO_ERROR, DataManagement, - "DUT Failure: Mandatory InvokeRequests field missing"); - VerifyOrDieWithMsg(mTimedRequest == isTimedInvoke, DataManagement, - "DUT Failure: TimedRequest value in message mismatches action"); - - { - InvokeRequestMessage::Parser validationInvokeRequestMessage = invokeRequestMessage; - VerifyOrDieWithMsg(ValidateInvokeRequestMessageAndBuildRegistry(validationInvokeRequestMessage) == CHIP_NO_ERROR, - DataManagement, "DUT Failure: InvokeRequestMessage contents were invalid"); - } - - TLV::TLVReader invokeRequestsReader; - invokeRequests.GetReader(&invokeRequestsReader); - - size_t commandCount = 0; - VerifyOrDieWithMsg(TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */) == CHIP_NO_ERROR, - DataManagement, - "TH Failure: Failed to get the length of InvokeRequests after InvokeRequestMessage validation"); - - // The command count check (specifically for a count of 2) is tied to IDM_1_3. This may need adjustment for - // compatibility with future test plans. - VerifyOrDieWithMsg(commandCount == 2, DataManagement, "DUT failure: We were strictly expecting exactly 2 InvokeRequests"); - mReserveSpaceForMoreChunkMessages = true; - - { - // Response path is the same as request path since we are replying with a failure message. - ConcreteCommandPath concreteResponsePath1; - ConcreteCommandPath concreteResponsePath2; - VerifyOrDieWithMsg( - TestOnlyExtractCommandPathFromNextInvokeRequest(invokeRequestsReader, concreteResponsePath1) == CHIP_NO_ERROR, - DataManagement, "DUT Failure: Issues encountered while extracting the ConcreteCommandPath from the first request"); - VerifyOrDieWithMsg( - TestOnlyExtractCommandPathFromNextInvokeRequest(invokeRequestsReader, concreteResponsePath2) == CHIP_NO_ERROR, - DataManagement, "DUT Failure: Issues encountered while extracting the ConcreteCommandPath from the second request"); - - if (faultType == NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder) - { - ConcreteCommandPath temp(concreteResponsePath1); - concreteResponsePath1 = concreteResponsePath2; - concreteResponsePath2 = temp; - } - - VerifyOrDieWithMsg(FallibleAddStatus(concreteResponsePath1, Status::Failure) == CHIP_NO_ERROR, DataManagement, - "TH Failure: Error adding the first InvokeResponse"); - if (faultType == NlFaultInjectionType::SeparateResponseMessages || - faultType == NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder) - { - VerifyOrDieWithMsg(FinalizeInvokeResponseMessageAndPrepareNext() == CHIP_NO_ERROR, DataManagement, - "TH Failure: Failed to create second InvokeResponseMessage"); - } - if (faultType != NlFaultInjectionType::SkipSecondResponse) - { - VerifyOrDieWithMsg(FallibleAddStatus(concreteResponsePath2, Status::Failure) == CHIP_NO_ERROR, DataManagement, - "TH Failure: Error adding the second InvokeResponse"); - } - } - - VerifyOrDieWithMsg(invokeRequestsReader.Next() == CHIP_END_OF_TLV, DataManagement, - "DUT Failure: Unexpected TLV ending of InvokeRequests"); - VerifyOrDieWithMsg(invokeRequestMessage.ExitContainer() == CHIP_NO_ERROR, DataManagement, - "DUT Failure: InvokeRequestMessage TLV is not properly terminated"); -} -#endif // CHIP_WITH_NLFAULTINJECTION - } // namespace app } // namespace chip diff --git a/src/app/CommandHandler.h b/src/app/CommandHandler.h index ae41ec1fe350fb..86f687c1c02466 100644 --- a/src/app/CommandHandler.h +++ b/src/app/CommandHandler.h @@ -15,78 +15,36 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -/** - * @file - * A handler for incoming Invoke interactions. - * - * Allows adding responses to be sent in an InvokeResponse: see the various - * "Add*" methods. - * - * Allows adding the responses asynchronously. See the documentation - * for the CommandHandler::Handle class below. - * - */ - #pragma once #include -#include #include #include #include #include -#include -#include -#include #include -#include #include -#include #include -#include -#include -#include -#include -#include -#include - -#include -#include namespace chip { namespace app { +/** + * A handler for incoming Invoke interactions. + * + * Allows adding responses to be sent in an InvokeResponse: see the various + * "Add*" methods. + * + * Allows adding the responses asynchronously when using `CommandHandler::Handle` + * (see documentation for `CommandHandler::Handle` for details) + * + * Upgrading notes: this class has moved to an interface from a previous more complex + * implementation. If upgrading code between versions, please see docs/upgrading.md + */ class CommandHandler { public: - class Callback - { - public: - virtual ~Callback() = default; - - /* - * Method that signals to a registered callback that this object - * has completed doing useful work and is now safe for release/destruction. - */ - virtual void OnDone(CommandHandler & apCommandObj) = 0; - - /* - * Upon processing of a CommandDataIB, this method is invoked to dispatch the command - * to the right server-side handler provided by the application. - */ - virtual void DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, - TLV::TLVReader & apPayload) = 0; - - /* - * Check to see if a command implementation exists for a specific - * concrete command path. If it does, Success will be returned. If - * not, one of UnsupportedEndpoint, UnsupportedCluster, or - * UnsupportedCommand will be returned, depending on how the command - * fails to exist. - */ - virtual Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) = 0; - }; + virtual ~CommandHandler() = default; /** * Class that allows asynchronous command processing before sending a @@ -156,157 +114,23 @@ class CommandHandler CommandHandler * mpHandler = nullptr; }; - // Previously we kept adding arguments with default values individually as parameters. This is because there - // is legacy code outside of the SDK that would call PrepareCommand. With the new PrepareInvokeResponseCommand - // replacing PrepareCommand, we took this opportunity to create a new parameter structure to make it easier to - // add new parameters without there needing to be an ever increasing parameter list with defaults. - struct InvokeResponseParameters - { - InvokeResponseParameters(const ConcreteCommandPath & aRequestCommandPath) : mRequestCommandPath(aRequestCommandPath) {} - - InvokeResponseParameters & SetStartOrEndDataStruct(bool aStartOrEndDataStruct) - { - mStartOrEndDataStruct = aStartOrEndDataStruct; - return *this; - } - - ConcreteCommandPath mRequestCommandPath; - /** - * Whether the method this is being provided to should start/end the TLV container for the CommandFields element - * within CommandDataIB. - */ - bool mStartOrEndDataStruct = true; - }; - - struct TestOnlyOverrides - { - public: - CommandPathRegistry * commandPathRegistry = nullptr; - CommandHandlerExchangeInterface * commandResponder = nullptr; - }; - - /* - * Constructor. - * - * The callback passed in has to outlive this CommandHandler object. - */ - CommandHandler(Callback * apCallback); - - /* - * Destructor. - * - * The call will also invalidate all Handles created for this CommandHandler. - * - */ - ~CommandHandler(); - - /* - * Constructor to override the number of supported paths per invoke and command responder. - * - * The callback and any pointers passed via TestOnlyOverrides must outlive this - * CommandHandler object. - * - * For testing purposes. - */ - CommandHandler(TestOnlyOverrides & aTestOverride, Callback * apCallback); - - /* - * Main entrypoint for this class to handle an InvokeRequestMessage. - * - * This function MAY call the registered OnDone callback before returning. - * To prevent immediate OnDone invocation, callers can wrap their CommandHandler instance - * within a CommandHandler::Handle. - * - * isTimedInvoke is true if and only if this is part of a Timed Invoke - * transaction (i.e. was preceded by a Timed Request). If we reach here, - * the timer verification has already been done. - * - * commandResponder handles sending InvokeResponses, added by clusters, to the client. The - * command responder object must outlive this CommandHandler object. It is only safe to - * release after the caller of OnInvokeCommandRequest receives the OnDone callback. - */ - Protocols::InteractionModel::Status OnInvokeCommandRequest(CommandHandlerExchangeInterface & commandResponder, - System::PacketBufferHandle && payload, bool isTimedInvoke); - - /** - * Checks that all CommandDataIB within InvokeRequests satisfy the spec's general - * constraints for CommandDataIB. Additionally checks that InvokeRequestMessage is - * properly formatted. - * - * This also builds a registry to ensure that all commands can be responded - * to with the data required as per spec. - */ - CHIP_ERROR ValidateInvokeRequestMessageAndBuildRegistry(InvokeRequestMessage::Parser & invokeRequestMessage); - /** * Adds the given command status and returns any failures in adding statuses (e.g. out * of buffer space) to the caller */ - CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::Status aStatus, - const char * context = nullptr); + virtual CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, + const Protocols::InteractionModel::Status aStatus, const char * context = nullptr) = 0; /** * Adds a status when the caller is unable to handle any failures. Logging is performed * and failure to register the status is checked with VerifyOrDie. */ - void AddStatus(const ConcreteCommandPath & aCommandPath, const Protocols::InteractionModel::Status aStatus, - const char * context = nullptr); + virtual void AddStatus(const ConcreteCommandPath & aCommandPath, const Protocols::InteractionModel::Status aStatus, + const char * context = nullptr) = 0; - CHIP_ERROR AddClusterSpecificSuccess(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus); + virtual CHIP_ERROR AddClusterSpecificSuccess(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) = 0; - CHIP_ERROR AddClusterSpecificFailure(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus); - - /** - * This adds a new CommandDataIB element into InvokeResponses for the associated - * aRequestCommandPath. This adds up until the `CommandFields` element within - * `CommandDataIB`. - * - * This call will fail if CommandHandler is already in the middle of building a - * CommandStatusIB or CommandDataIB (i.e. something has called Prepare*, without - * calling Finish*), or is already sending InvokeResponseMessage. - * - * Upon success, the caller is expected to call `FinishCommand` once they have added - * all the fields into the CommandFields element of CommandDataIB. - * - * @param [in] aResponseCommandPath the concrete response path that we are sending to Requester. - * @param [in] aPrepareParameters struct containing paramters needs for preparing a command. Data - * such as request path, and whether this method should start the CommandFields element within - * CommandDataIB. - */ - CHIP_ERROR PrepareInvokeResponseCommand(const ConcreteCommandPath & aResponseCommandPath, - const InvokeResponseParameters & aPrepareParameters); - - [[deprecated("PrepareCommand now needs the requested command path. Please use PrepareInvokeResponseCommand")]] CHIP_ERROR - PrepareCommand(const ConcreteCommandPath & aCommandPath, bool aStartDataStruct = true); - - /** - * Finishes the CommandDataIB element within the InvokeResponses. - * - * Caller must have first successfully called `PrepareInvokeResponseCommand`. - * - * @param [in] aEndDataStruct end the TLV container for the CommandFields element within - * CommandDataIB. This should match the boolean passed into Prepare*. - * - * @return CHIP_ERROR_INCORRECT_STATE - * If device has not previously successfully called - * `PrepareInvokeResponseCommand`. - * @return CHIP_ERROR_BUFFER_TOO_SMALL - * If writing the values needed to finish the InvokeReponseIB - * with the current contents of the InvokeResponseMessage - * would exceed the limit. When this error occurs, it is possible - * we have already closed some of the IB Builders that were - * previously started in `PrepareInvokeResponseCommand`. - * @return CHIP_ERROR_NO_MEMORY - * If TLVWriter attempted to allocate an output buffer failed due to - * lack of memory. - * @return other Other TLVWriter related errors. Typically occurs if - * `GetCommandDataIBTLVWriter()` was called and used incorrectly. - */ - // TODO(#30453): We should be able to eliminate the chances of OOM issues with reserve. - // This will be completed in a follow up PR. - CHIP_ERROR FinishCommand(bool aEndDataStruct = true); - - TLV::TLVWriter * GetCommandDataIBTLVWriter(); + virtual CHIP_ERROR AddClusterSpecificFailure(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) = 0; /** * GetAccessingFabricIndex() may only be called during synchronous command @@ -314,24 +138,7 @@ class CommandHandler * CommandHandler::Handle or equivalent) must not call this method, because * it will not work right if the session we're using was evicted. */ - FabricIndex GetAccessingFabricIndex() const; - - /** - * API for adding a data response. The template parameter T is generally - * expected to be a ClusterName::Commands::CommandName::Type struct, but any - * object that can be encoded using the DataModel::Encode machinery and - * exposes the right command id will work. - * - * @param [in] aRequestCommandPath the concrete path of the command we are - * responding to. - * @param [in] aData the data for the response. - */ - template - CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData) - { - DataModel::EncodableType encoder(aData); - return AddResponseData(aRequestCommandPath, CommandData::GetCommandId(), encoder); - } + virtual FabricIndex GetAccessingFabricIndex() const = 0; /** * API for adding a data response. The `aEncodable` is generally expected to encode @@ -343,58 +150,48 @@ class CommandHandler * @param [in] aEncodable - an encodable that places the command data structure * for `aResponseCommandId` into a TLV Writer. * - * Most applications are likely to use `AddResponseData` as a more convenient - * one-call that auto-sets command ID and creates the underlying encoders. + * If you have no great way of handling the returned CHIP_ERROR, consider + * using `AddResponse` which will automatically reply with `Failure` in + * case AddResponseData fails. */ - CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - DataModel::EncodableToTLV & aEncodable) - { - // Return early when response should not be sent out. - VerifyOrReturnValue(ResponsesAccepted(), CHIP_NO_ERROR); - return TryAddingResponse( - [&]() -> CHIP_ERROR { return TryAddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); }); - } + virtual CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) = 0; /** - * API for adding a response. This will try to encode a data response (response command), and if that fails will encode a a - * Protocols::InteractionModel::Status::Failure status response instead. + * Attempts to encode a response to a command. * - * The template parameter T is generally expected to be a ClusterName::Commands::CommandName::Type struct, but any object that - * can be encoded using the DataModel::Encode machinery and exposes the right command id will work. + * `aRequestCommandPath` represents the request path (endpoint/cluster/commandid) and the reply + * will preserve the same path and switch the command id to aResponseCommandId. * - * Since the function will call AddStatus when it fails to encode the data, it cannot send any response when it fails to encode - * a status code since another AddStatus call will also fail. The error from AddStatus will just be logged. + * As this command does not return any error codes, it must try its best to encode the reply + * and if it fails, it MUST encode a `Protocols::InteractionModel::Status::Failure` as a + * reply (i.e. a reply is guaranteed to be sent). * - * @param [in] aRequestCommandPath the concrete path of the command we are - * responding to. - * @param [in] aData the data for the response. + * Above is the main difference from AddResponseData: AddResponse will auto-reply with failure while + * AddResponseData allows the caller to try to deal with any CHIP_ERRORs. */ - template - void AddResponse(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData) - { - DataModel::EncodableType encodable(aData); - return AddResponse(aRequestCommandPath, CommandData::GetCommandId(), encodable); - } + virtual void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) = 0; /** - * API for adding a response with a given encodable of TLV data. - * - * The encodable would generally encode a ClusterName::Commands::CommandName::Type with - * the corresponding `GetCommandId` call. + * Check whether the InvokeRequest we are handling is a timed invoke. */ - void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - DataModel::EncodableToTLV & aEncodable) - { - if (AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable) != CHIP_NO_ERROR) - { - AddStatus(aRequestCommandPath, Protocols::InteractionModel::Status::Failure); - } - } + virtual bool IsTimedInvoke() const = 0; /** - * Check whether the InvokeRequest we are handling is a timed invoke. + * @brief Flush acks right away for a slow command + * + * Some commands that do heavy lifting of storage/crypto should + * ack right away to improve reliability and reduce needless retries. This + * method can be manually called in commands that are especially slow to + * immediately schedule an acknowledgement (if needed) since the delayed + * stand-alone ack timer may actually not hit soon enough due to blocking command + * execution. + * */ - bool IsTimedInvoke() const { return mTimedRequest; } + virtual void FlushAcksRightAwayOnSlowCommand() = 0; + + virtual Access::SubjectDescriptor GetSubjectDescriptor() const = 0; /** * Gets the inner exchange context object, without ownership. @@ -407,310 +204,74 @@ class CommandHandler * exchange context has been assigned or the context * has been released. */ - Messaging::ExchangeContext * GetExchangeContext() const - { - VerifyOrDie(mpResponder); - return mpResponder->GetExchangeContext(); - } + virtual Messaging::ExchangeContext * GetExchangeContext() const = 0; /** - * @brief Flush acks right away for a slow command + * API for adding a data response. The template parameter T is generally + * expected to be a ClusterName::Commands::CommandName::Type struct, but any + * object that can be encoded using the DataModel::Encode machinery and + * exposes the right command id will work. * - * Some commands that do heavy lifting of storage/crypto should - * ack right away to improve reliability and reduce needless retries. This - * method can be manually called in commands that are especially slow to - * immediately schedule an acknowledgement (if needed) since the delayed - * stand-alone ack timer may actually not hit soon enough due to blocking command - * execution. + * If you have no great way of handling the returned CHIP_ERROR, consider + * using `AddResponse` which will automatically reply with `Failure` in + * case AddResponseData fails. + * + * @param [in] aRequestCommandPath the concrete path of the command we are + * responding to. * + * The response path will be the same as the request, except the + * reply command ID used will be `CommandData::GetCommandId()` assumed + * to be a member of the templated type + * + * @param [in] aData the data for the response. It is expected to provide + * `GetCommandData` as a STATIC on its type as well as encode the + * correct data structure for building a reply. */ - void FlushAcksRightAwayOnSlowCommand() + template + CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData) { - if (mpResponder) - { - mpResponder->HandlingSlowCommand(); - } + DataModel::EncodableType encoder(aData); + return AddResponseData(aRequestCommandPath, CommandData::GetCommandId(), encoder); } /** - * GetSubjectDescriptor() may only be called during synchronous command - * processing. Anything that runs async (while holding a - * CommandHandler::Handle or equivalent) must not call this method, because - * it might not work right if the session we're using was evicted. - */ - Access::SubjectDescriptor GetSubjectDescriptor() const - { - VerifyOrDie(!mGoneAsync); - VerifyOrDie(mpResponder); - return mpResponder->GetSubjectDescriptor(); - } - -#if CHIP_WITH_NLFAULTINJECTION - - enum class NlFaultInjectionType : uint8_t - { - SeparateResponseMessages, - SeparateResponseMessagesAndInvertedResponseOrder, - SkipSecondResponse - }; - - /** - * @brief Sends InvokeResponseMessages with injected faults for certification testing. - * - * The Test Harness (TH) uses this to simulate various server response behaviors, - * ensuring the Device Under Test (DUT) handles responses per specification. + * API for adding a response. This will try to encode a data response (response command), and if that fails + * it will encode a Protocols::InteractionModel::Status::Failure status response instead. * - * This function strictly validates the DUT's InvokeRequestMessage against the test plan. - * If deviations occur, the TH terminates with a detailed error message. + * Above is the main difference from AddResponseData: AddResponse will auto-reply with failure while + * AddResponseData allows the caller to try to deal with any CHIP_ERRORs. * - * @param commandResponder commandResponder that will send the InvokeResponseMessages to the client. - * @param payload Payload of the incoming InvokeRequestMessage from the client. - * @param isTimedInvoke Indicates whether the interaction is timed. - * @param faultType The specific type of fault to inject into the response. - */ - // TODO(#30453): After refactoring CommandHandler for better unit testability, create a - // unit test specifically for the fault injection behavior. - void TestOnlyInvokeCommandRequestWithFaultsInjected(CommandHandlerExchangeInterface & commandResponder, - System::PacketBufferHandle && payload, bool isTimedInvoke, - NlFaultInjectionType faultType); -#endif // CHIP_WITH_NLFAULTINJECTION - -private: - friend class TestCommandInteraction; - friend class CommandHandler::Handle; - - enum class State : uint8_t - { - Idle, ///< Default state that the object starts out in, where no work has commenced - NewResponseMessage, ///< mInvokeResponseBuilder is ready, with no responses added. - Preparing, ///< We are prepaing the command or status header. - AddingCommand, ///< In the process of adding a command. - AddedCommand, ///< A command has been completely encoded and is awaiting transmission. - DispatchResponses, ///< The command response(s) are being dispatched. - AwaitingDestruction, ///< The object has completed its work and is awaiting destruction by the application. - }; - - /** - * @brief Best effort to add InvokeResponse to InvokeResponseMessage. + * The template parameter T is generally expected to be a ClusterName::Commands::CommandName::Type struct, but any object that + * can be encoded using the DataModel::Encode machinery and exposes the right command id will work. * - * Tries to add response using lambda. Upon failure to add response, attempts - * to rollback the InvokeResponseMessage to a known good state. If failure is due - * to insufficient space in the current InvokeResponseMessage: - * - Finalizes the current InvokeResponseMessage. - * - Allocates a new InvokeResponseMessage. - * - Reattempts to add the InvokeResponse to the new InvokeResponseMessage. + * Since the function will call AddStatus when it fails to encode the data, it cannot send any response when it fails to encode + * a status code since another AddStatus call will also fail. The error from AddStatus will just be logged. * - * @param [in] addResponseFunction A lambda function responsible for adding the - * response to the current InvokeResponseMessage. + * @param [in] aRequestCommandPath the concrete path of the command we are + * responding to. + * @param [in] aData the data for the response. */ - template - CHIP_ERROR TryAddingResponse(Function && addResponseFunction) + template + void AddResponse(const ConcreteCommandPath & aRequestCommandPath, const CommandData & aData) { - // Invalidate any existing rollback backups. The addResponseFunction is - // expected to create a new backup during either PrepareInvokeResponseCommand - // or PrepareStatus execution. Direct invocation of - // CreateBackupForResponseRollback is avoided since the buffer used by - // InvokeResponseMessage might not be allocated until a Prepare* function - // is called. - mRollbackBackupValid = false; - CHIP_ERROR err = addResponseFunction(); - if (err == CHIP_NO_ERROR) - { - return CHIP_NO_ERROR; - } - ReturnErrorOnFailure(RollbackResponse()); - // If we failed to add a command due to lack of space in the - // packet, we will make another attempt to add the response using - // an additional InvokeResponseMessage. - if (mState != State::AddedCommand || err != CHIP_ERROR_NO_MEMORY) - { - return err; - } - ReturnErrorOnFailure(FinalizeInvokeResponseMessageAndPrepareNext()); - err = addResponseFunction(); - if (err != CHIP_NO_ERROR) - { - // The return value of RollbackResponse is ignored, as we prioritize - // conveying the error generated by addResponseFunction to the - // caller. - RollbackResponse(); - } - return err; + DataModel::EncodableType encodable(aData); + AddResponse(aRequestCommandPath, CommandData::GetCommandId(), encodable); } - void MoveToState(const State aTargetState); - const char * GetStateStr() const; - - /** - * Create a backup to enable rolling back to the state prior to ResponseData encoding in the event of failure. - */ - void CreateBackupForResponseRollback(); - - /** - * Rollback the state to before encoding the current ResponseData (before calling PrepareInvokeResponseCommand / PrepareStatus) - * - * Requires CreateBackupForResponseRollback to be called at the start of PrepareInvokeResponseCommand / PrepareStatus - */ - CHIP_ERROR RollbackResponse(); - - /* - * This forcibly closes the exchange context if a valid one is pointed to. Such a situation does - * not arise during normal message processing flows that all normally call Close() above. This can only - * arise due to application-initiated destruction of the object when this object is handling receiving/sending - * message payloads. - */ - void Abort(); - +protected: /** * IncrementHoldOff will increase the inner refcount of the CommandHandler. * * Users should use CommandHandler::Handle for management the lifespan of the CommandHandler. * DefRef should be released in reasonable time, and Close() should only be called when the refcount reached 0. */ - void IncrementHoldOff(Handle * apHandle); + virtual void IncrementHoldOff(Handle * apHandle) {} /** * DecrementHoldOff is used by CommandHandler::Handle for decreasing the refcount of the CommandHandler. * When refcount reached 0, CommandHandler will send the response to the peer and shutdown. */ - void DecrementHoldOff(Handle * apHandle); - - /* - * Allocates a packet buffer used for encoding an invoke response payload. - * - * This can be called multiple times safely, as it will only allocate the buffer once for the lifetime - * of this object. - */ - CHIP_ERROR AllocateBuffer(); - - /** - * This will add a new CommandStatusIB element into InvokeResponses. It will put the - * aCommandPath into the CommandPath element within CommandStatusIB. - * - * This call will fail if CommandHandler is already in the middle of building a - * CommandStatusIB or CommandDataIB (i.e. something has called Prepare*, without - * calling Finish*), or is already sending InvokeResponseMessage. - * - * Upon success, the caller is expected to call `FinishStatus` once they have encoded - * StatusIB. - * - * @param [in] aCommandPath the concrete path of the command we are responding to. - */ - CHIP_ERROR PrepareStatus(const ConcreteCommandPath & aCommandPath); - - /** - * Finishes the CommandStatusIB element within the InvokeResponses. - * - * Caller must have first successfully called `PrepareStatus`. - */ - CHIP_ERROR FinishStatus(); - - CHIP_ERROR PrepareInvokeResponseCommand(const CommandPathRegistryEntry & apCommandPathRegistryEntry, - const ConcreteCommandPath & aCommandPath, bool aStartDataStruct); - - CHIP_ERROR FinalizeLastInvokeResponseMessage() { return FinalizeInvokeResponseMessage(/* aHasMoreChunks = */ false); } - - CHIP_ERROR FinalizeInvokeResponseMessageAndPrepareNext(); - - CHIP_ERROR FinalizeInvokeResponseMessage(bool aHasMoreChunks); - - Protocols::InteractionModel::Status ProcessInvokeRequest(System::PacketBufferHandle && payload, bool isTimedInvoke); - - /** - * Called internally to signal the completion of all work on this object, gracefully close the - * exchange (by calling into the base class) and finally, signal to a registerd callback that it's - * safe to release this object. - */ - void Close(); - - /** - * ProcessCommandDataIB is only called when a unicast invoke command request is received - * It requires the endpointId in its command path to be able to dispatch the command - */ - Protocols::InteractionModel::Status ProcessCommandDataIB(CommandDataIB::Parser & aCommandElement); - - /** - * ProcessGroupCommandDataIB is only called when a group invoke command request is received - * It doesn't need the endpointId in it's command path since it uses the GroupId in message metadata to find it - */ - Protocols::InteractionModel::Status ProcessGroupCommandDataIB(CommandDataIB::Parser & aCommandElement); - - CHIP_ERROR TryAddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus); - - CHIP_ERROR AddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus); - - /** - * If this function fails, it may leave our TLV buffer in an inconsistent state. - * Callers should snapshot as needed before calling this function, and roll back - * as needed afterward. - * - * @param [in] aRequestCommandPath the concrete path of the command we are responding to - * @param [in] aResponseCommandId the id of the command to encode - * @param [in] aEncodable the data to encode for the given aResponseCommandId - */ - CHIP_ERROR TryAddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, - DataModel::EncodableToTLV & aEncodable); - - void SetExchangeInterface(CommandHandlerExchangeInterface * commandResponder); - - /** - * Check whether the InvokeRequest we are handling is targeted to a group. - */ - bool IsGroupRequest() { return mGroupRequest; } - - bool ResponsesAccepted() { return !(mGroupRequest || mpResponder == nullptr); } - - /** - * Sets the state flag to keep the information that request we are handling is targeted to a group. - */ - void SetGroupRequest(bool isGroupRequest) { mGroupRequest = isGroupRequest; } - - CommandPathRegistry & GetCommandPathRegistry() const { return *mCommandPathRegistry; } - - size_t MaxPathsPerInvoke() const { return mMaxPathsPerInvoke; } - - void AddToHandleList(Handle * handle); - - void RemoveFromHandleList(Handle * handle); - - void InvalidateHandles(); - - bool TestOnlyIsInIdleState() const { return mState == State::Idle; } - - Callback * mpCallback = nullptr; - InvokeResponseMessage::Builder mInvokeResponseBuilder; - TLV::TLVType mDataElementContainerType = TLV::kTLVType_NotSpecified; - size_t mPendingWork = 0; - /* List to store all currently-outstanding Handles for this Command Handler.*/ - IntrusiveList mpHandleList; - - chip::System::PacketBufferTLVWriter mCommandMessageWriter; - TLV::TLVWriter mBackupWriter; - size_t mMaxPathsPerInvoke = CHIP_CONFIG_MAX_PATHS_PER_INVOKE; - // TODO(#30453): See if we can reduce this size for the default cases - // TODO Allow flexibility in registration. - BasicCommandPathRegistry mBasicCommandPathRegistry; - CommandPathRegistry * mCommandPathRegistry = &mBasicCommandPathRegistry; - std::optional mRefForResponse; - - CommandHandlerExchangeInterface * mpResponder = nullptr; - - State mState = State::Idle; - State mBackupState; - ScopedChangeOnly mInternalCallToAddResponseData{ false }; - bool mSuppressResponse = false; - bool mTimedRequest = false; - bool mGroupRequest = false; - bool mBufferAllocated = false; - bool mReserveSpaceForMoreChunkMessages = false; - // TODO(#32486): We should introduce breaking change where calls to add CommandData - // need to use AddResponse, and not CommandHandler primitives directly using - // GetCommandDataIBTLVWriter. - bool mRollbackBackupValid = false; - // If mGoneAsync is true, we have finished out initial processing of the - // incoming invoke. After this point, our session could go away at any - // time. - bool mGoneAsync = false; + virtual void DecrementHoldOff(Handle * apHandle) {} }; } // namespace app diff --git a/src/app/CommandHandlerImpl.cpp b/src/app/CommandHandlerImpl.cpp new file mode 100644 index 00000000000000..f32d6bebd49455 --- /dev/null +++ b/src/app/CommandHandlerImpl.cpp @@ -0,0 +1,1057 @@ +/* + * + * Copyright (c) 2020 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { +using Status = Protocols::InteractionModel::Status; + +CommandHandlerImpl::CommandHandlerImpl(Callback * apCallback) : mpCallback(apCallback), mSuppressResponse(false) {} + +CommandHandlerImpl::CommandHandlerImpl(TestOnlyOverrides & aTestOverride, Callback * apCallback) : CommandHandlerImpl(apCallback) +{ + if (aTestOverride.commandPathRegistry) + { + mMaxPathsPerInvoke = aTestOverride.commandPathRegistry->MaxSize(); + mCommandPathRegistry = aTestOverride.commandPathRegistry; + } + if (aTestOverride.commandResponder) + { + SetExchangeInterface(aTestOverride.commandResponder); + } +} + +CommandHandlerImpl::~CommandHandlerImpl() +{ + InvalidateHandles(); +} + +CHIP_ERROR CommandHandlerImpl::AllocateBuffer() +{ + // We should only allocate a buffer if we will be sending out a response. + VerifyOrReturnError(ResponsesAccepted(), CHIP_ERROR_INCORRECT_STATE); + + if (!mBufferAllocated) + { + mCommandMessageWriter.Reset(); + + System::PacketBufferHandle commandPacket = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes); + VerifyOrReturnError(!commandPacket.IsNull(), CHIP_ERROR_NO_MEMORY); + + mCommandMessageWriter.Init(std::move(commandPacket)); + ReturnErrorOnFailure(mInvokeResponseBuilder.InitWithEndBufferReserved(&mCommandMessageWriter)); + + if (mReserveSpaceForMoreChunkMessages) + { + ReturnErrorOnFailure(mInvokeResponseBuilder.ReserveSpaceForMoreChunkedMessages()); + } + + // Sending an InvokeResponse to an InvokeResponse is going to be removed from the spec soon. + // It was never implemented in the SDK, and there are no command responses that expect a + // command response. This means we will never receive an InvokeResponse Message in response + // to an InvokeResponse Message that we are sending. This means that the only response + // we are expecting to receive in response to an InvokeResponse Message that we are + // sending-out is a status when we are chunking multiple responses. As a result, to satisfy the + // condition that we don't set SuppressResponse to true while also setting + // MoreChunkedMessages to true, we are hardcoding the value to false here. + mInvokeResponseBuilder.SuppressResponse(/* aSuppressResponse = */ false); + ReturnErrorOnFailure(mInvokeResponseBuilder.GetError()); + + mInvokeResponseBuilder.CreateInvokeResponses(/* aReserveEndBuffer = */ true); + ReturnErrorOnFailure(mInvokeResponseBuilder.GetError()); + + mBufferAllocated = true; + MoveToState(State::NewResponseMessage); + } + + return CHIP_NO_ERROR; +} + +Status CommandHandlerImpl::OnInvokeCommandRequest(CommandHandlerExchangeInterface & commandResponder, + System::PacketBufferHandle && payload, bool isTimedInvoke) +{ + VerifyOrDieWithMsg(mState == State::Idle, DataManagement, "state should be Idle"); + + SetExchangeInterface(&commandResponder); + + // Using RAII here: if this is the only handle remaining, DecrementHoldOff will + // call the CommandHandlerImpl::OnDone callback when this function returns. + Handle workHandle(this); + + Status status = ProcessInvokeRequest(std::move(payload), isTimedInvoke); + mGoneAsync = true; + return status; +} + +CHIP_ERROR CommandHandlerImpl::TryAddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) +{ + ConcreteCommandPath responseCommandPath = { aRequestCommandPath.mEndpointId, aRequestCommandPath.mClusterId, + aResponseCommandId }; + + InvokeResponseParameters prepareParams(aRequestCommandPath); + prepareParams.SetStartOrEndDataStruct(false); + + { + ScopedChange internalCallToAddResponse(mInternalCallToAddResponseData, true); + ReturnErrorOnFailure(PrepareInvokeResponseCommand(responseCommandPath, prepareParams)); + } + + TLV::TLVWriter * writer = GetCommandDataIBTLVWriter(); + VerifyOrReturnError(writer != nullptr, CHIP_ERROR_INCORRECT_STATE); + ReturnErrorOnFailure(aEncodable.EncodeTo(*writer, TLV::ContextTag(CommandDataIB::Tag::kFields))); + return FinishCommand(/* aEndDataStruct = */ false); +} + +CHIP_ERROR CommandHandlerImpl::AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) +{ + // Return early when response should not be sent out. + VerifyOrReturnValue(ResponsesAccepted(), CHIP_NO_ERROR); + return TryAddingResponse( + [&]() -> CHIP_ERROR { return TryAddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); }); +} + +CHIP_ERROR CommandHandlerImpl::ValidateInvokeRequestMessageAndBuildRegistry(InvokeRequestMessage::Parser & invokeRequestMessage) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + size_t commandCount = 0; + bool commandRefExpected = false; + InvokeRequests::Parser invokeRequests; + + ReturnErrorOnFailure(invokeRequestMessage.GetInvokeRequests(&invokeRequests)); + TLV::TLVReader invokeRequestsReader; + invokeRequests.GetReader(&invokeRequestsReader); + + ReturnErrorOnFailure(TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */)); + + // If this is a GroupRequest the only thing to check is that there is only one + // CommandDataIB. + if (IsGroupRequest()) + { + VerifyOrReturnError(commandCount == 1, CHIP_ERROR_INVALID_ARGUMENT); + return CHIP_NO_ERROR; + } + // While technically any commandCount == 1 should already be unique and does not need + // any further validation, we do need to read and populate the registry to help + // in building the InvokeResponse. + + VerifyOrReturnError(commandCount <= MaxPathsPerInvoke(), CHIP_ERROR_INVALID_ARGUMENT); + + // If there is more than one CommandDataIB, spec states that CommandRef must be provided. + commandRefExpected = commandCount > 1; + + while (CHIP_NO_ERROR == (err = invokeRequestsReader.Next())) + { + VerifyOrReturnError(TLV::AnonymousTag() == invokeRequestsReader.GetTag(), CHIP_ERROR_INVALID_ARGUMENT); + CommandDataIB::Parser commandData; + ReturnErrorOnFailure(commandData.Init(invokeRequestsReader)); + + // First validate that we can get a ConcreteCommandPath. + CommandPathIB::Parser commandPath; + ConcreteCommandPath concretePath(0, 0, 0); + ReturnErrorOnFailure(commandData.GetPath(&commandPath)); + ReturnErrorOnFailure(commandPath.GetConcreteCommandPath(concretePath)); + + // Grab the CommandRef if there is one, and validate that it's there when it + // has to be. + std::optional commandRef; + uint16_t ref; + err = commandData.GetRef(&ref); + VerifyOrReturnError(err == CHIP_NO_ERROR || err == CHIP_END_OF_TLV, err); + if (err == CHIP_END_OF_TLV && commandRefExpected) + { + return CHIP_ERROR_INVALID_ARGUMENT; + } + if (err == CHIP_NO_ERROR) + { + commandRef.emplace(ref); + } + + // Adding can fail if concretePath is not unique, or if commandRef is a value + // and is not unique, or if we have already added more paths than we support. + ReturnErrorOnFailure(GetCommandPathRegistry().Add(concretePath, commandRef)); + } + + // It's OK/expected to have reached the end of the container without failure. + if (CHIP_END_OF_TLV == err) + { + err = CHIP_NO_ERROR; + } + ReturnErrorOnFailure(err); + return invokeRequestMessage.ExitContainer(); +} + +Status CommandHandlerImpl::ProcessInvokeRequest(System::PacketBufferHandle && payload, bool isTimedInvoke) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + System::PacketBufferTLVReader reader; + InvokeRequestMessage::Parser invokeRequestMessage; + InvokeRequests::Parser invokeRequests; + reader.Init(std::move(payload)); + VerifyOrReturnError(invokeRequestMessage.Init(reader) == CHIP_NO_ERROR, Status::InvalidAction); +#if CHIP_CONFIG_IM_PRETTY_PRINT + invokeRequestMessage.PrettyPrint(); +#endif + VerifyOrDie(mpResponder); + if (mpResponder->GetGroupId().HasValue()) + { + SetGroupRequest(true); + } + + // When updating this code, please remember to make corresponding changes to TestOnlyInvokeCommandRequestWithFaultsInjected. + VerifyOrReturnError(invokeRequestMessage.GetSuppressResponse(&mSuppressResponse) == CHIP_NO_ERROR, Status::InvalidAction); + VerifyOrReturnError(invokeRequestMessage.GetTimedRequest(&mTimedRequest) == CHIP_NO_ERROR, Status::InvalidAction); + VerifyOrReturnError(invokeRequestMessage.GetInvokeRequests(&invokeRequests) == CHIP_NO_ERROR, Status::InvalidAction); + VerifyOrReturnError(mTimedRequest == isTimedInvoke, Status::TimedRequestMismatch); + + { + InvokeRequestMessage::Parser validationInvokeRequestMessage = invokeRequestMessage; + VerifyOrReturnError(ValidateInvokeRequestMessageAndBuildRegistry(validationInvokeRequestMessage) == CHIP_NO_ERROR, + Status::InvalidAction); + } + + TLV::TLVReader invokeRequestsReader; + invokeRequests.GetReader(&invokeRequestsReader); + + size_t commandCount = 0; + VerifyOrReturnError(TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */) == CHIP_NO_ERROR, + Status::InvalidAction); + if (commandCount > 1) + { + mReserveSpaceForMoreChunkMessages = true; + } + + while (CHIP_NO_ERROR == (err = invokeRequestsReader.Next())) + { + VerifyOrReturnError(TLV::AnonymousTag() == invokeRequestsReader.GetTag(), Status::InvalidAction); + CommandDataIB::Parser commandData; + VerifyOrReturnError(commandData.Init(invokeRequestsReader) == CHIP_NO_ERROR, Status::InvalidAction); + Status status = Status::Success; + if (IsGroupRequest()) + { + status = ProcessGroupCommandDataIB(commandData); + } + else + { + status = ProcessCommandDataIB(commandData); + } + if (status != Status::Success) + { + return status; + } + } + + // if we have exhausted this container + if (CHIP_END_OF_TLV == err) + { + err = CHIP_NO_ERROR; + } + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); + VerifyOrReturnError(invokeRequestMessage.ExitContainer() == CHIP_NO_ERROR, Status::InvalidAction); + return Status::Success; +} + +void CommandHandlerImpl::Close() +{ + mSuppressResponse = false; + mpResponder = nullptr; + MoveToState(State::AwaitingDestruction); + + // We must finish all async work before we can shut down a CommandHandlerImpl. The actual CommandHandlerImpl MUST finish their + // work in reasonable time or there is a bug. The only case for releasing CommandHandlerImpl without CommandHandler::Handle + // releasing its reference is the stack shutting down, in which case Close() is not called. So the below check should always + // pass. + VerifyOrDieWithMsg(mPendingWork == 0, DataManagement, "CommandHandlerImpl::Close() called with %u unfinished async work items", + static_cast(mPendingWork)); + InvalidateHandles(); + + if (mpCallback) + { + mpCallback->OnDone(*this); + } +} + +void CommandHandlerImpl::AddToHandleList(Handle * apHandle) +{ + mpHandleList.PushBack(apHandle); +} + +void CommandHandlerImpl::RemoveFromHandleList(Handle * apHandle) +{ + VerifyOrDie(mpHandleList.Contains(apHandle)); + mpHandleList.Remove(apHandle); +} + +void CommandHandlerImpl::InvalidateHandles() +{ + for (auto handle = mpHandleList.begin(); handle != mpHandleList.end(); ++handle) + { + handle->Invalidate(); + } +} + +void CommandHandlerImpl::IncrementHoldOff(Handle * apHandle) +{ + mPendingWork++; + AddToHandleList(apHandle); +} + +void CommandHandlerImpl::DecrementHoldOff(Handle * apHandle) +{ + + mPendingWork--; + ChipLogDetail(DataManagement, "Decreasing reference count for CommandHandlerImpl, remaining %u", + static_cast(mPendingWork)); + + RemoveFromHandleList(apHandle); + + if (mPendingWork != 0) + { + return; + } + + if (mpResponder == nullptr) + { + ChipLogProgress(DataManagement, "Skipping command response: response sender is null"); + } + else if (!IsGroupRequest()) + { + CHIP_ERROR err = FinalizeLastInvokeResponseMessage(); + if (err != CHIP_NO_ERROR) + { + ChipLogError(DataManagement, "Failed to finalize command response: %" CHIP_ERROR_FORMAT, err.Format()); + } + } + + Close(); +} + +namespace { +// We use this when the sender did not actually provide a CommandFields struct, +// to avoid downstream consumers having to worry about cases when there is or is +// not a struct available. We use an empty struct with anonymous tag, since we +// can't use a context tag at top level, and consumers should not care about the +// tag here). +constexpr uint8_t sNoFields[] = { + CHIP_TLV_STRUCTURE(CHIP_TLV_TAG_ANONYMOUS), + CHIP_TLV_END_OF_CONTAINER, +}; +} // anonymous namespace + +Status CommandHandlerImpl::ProcessCommandDataIB(CommandDataIB::Parser & aCommandElement) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + CommandPathIB::Parser commandPath; + ConcreteCommandPath concretePath(0, 0, 0); + TLV::TLVReader commandDataReader; + + // NOTE: errors may occur before the concrete command path is even fully decoded. + + err = aCommandElement.GetPath(&commandPath); + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); + + err = commandPath.GetConcreteCommandPath(concretePath); + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); + + { + Status commandExists = mpCallback->CommandExists(concretePath); + if (commandExists != Status::Success) + { + ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x", + ChipLogValueMEI(concretePath.mCommandId), ChipLogValueMEI(concretePath.mClusterId), + concretePath.mEndpointId); + return FallibleAddStatus(concretePath, commandExists) != CHIP_NO_ERROR ? Status::Failure : Status::Success; + } + } + + { + Access::SubjectDescriptor subjectDescriptor = GetSubjectDescriptor(); + Access::RequestPath requestPath{ .cluster = concretePath.mClusterId, .endpoint = concretePath.mEndpointId }; + Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath); + err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege); + if (err != CHIP_NO_ERROR) + { + if (err != CHIP_ERROR_ACCESS_DENIED) + { + return FallibleAddStatus(concretePath, Status::Failure) != CHIP_NO_ERROR ? Status::Failure : Status::Success; + } + // TODO: when wildcard invokes are supported, handle them to discard rather than fail with status + return FallibleAddStatus(concretePath, Status::UnsupportedAccess) != CHIP_NO_ERROR ? Status::Failure : Status::Success; + } + } + + if (CommandNeedsTimedInvoke(concretePath.mClusterId, concretePath.mCommandId) && !IsTimedInvoke()) + { + // TODO: when wildcard invokes are supported, discard a + // wildcard-expanded path instead of returning a status. + return FallibleAddStatus(concretePath, Status::NeedsTimedInteraction) != CHIP_NO_ERROR ? Status::Failure : Status::Success; + } + + if (CommandIsFabricScoped(concretePath.mClusterId, concretePath.mCommandId)) + { + // SPEC: Else if the command in the path is fabric-scoped and there is no accessing fabric, + // a CommandStatusIB SHALL be generated with the UNSUPPORTED_ACCESS Status Code. + + // Fabric-scoped commands are not allowed before a specific accessing fabric is available. + // This is mostly just during a PASE session before AddNOC. + if (GetAccessingFabricIndex() == kUndefinedFabricIndex) + { + // TODO: when wildcard invokes are supported, discard a + // wildcard-expanded path instead of returning a status. + return FallibleAddStatus(concretePath, Status::UnsupportedAccess) != CHIP_NO_ERROR ? Status::Failure : Status::Success; + } + } + + err = aCommandElement.GetFields(&commandDataReader); + if (CHIP_END_OF_TLV == err) + { + ChipLogDetail(DataManagement, + "Received command without data for Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, + concretePath.mEndpointId, ChipLogValueMEI(concretePath.mClusterId), ChipLogValueMEI(concretePath.mCommandId)); + commandDataReader.Init(sNoFields); + err = commandDataReader.Next(); + } + if (CHIP_NO_ERROR == err) + { + ChipLogDetail(DataManagement, "Received command for Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, + concretePath.mEndpointId, ChipLogValueMEI(concretePath.mClusterId), ChipLogValueMEI(concretePath.mCommandId)); + SuccessOrExit(err = DataModelCallbacks::GetInstance()->PreCommandReceived(concretePath, GetSubjectDescriptor())); + mpCallback->DispatchCommand(*this, concretePath, commandDataReader); + DataModelCallbacks::GetInstance()->PostCommandReceived(concretePath, GetSubjectDescriptor()); + } + +exit: + if (err != CHIP_NO_ERROR) + { + return FallibleAddStatus(concretePath, Status::InvalidCommand) != CHIP_NO_ERROR ? Status::Failure : Status::Success; + } + + // We have handled the error status above and put the error status in response, now return success status so we can process + // other commands in the invoke request. + return Status::Success; +} + +Status CommandHandlerImpl::ProcessGroupCommandDataIB(CommandDataIB::Parser & aCommandElement) +{ + CHIP_ERROR err = CHIP_NO_ERROR; + CommandPathIB::Parser commandPath; + TLV::TLVReader commandDataReader; + ClusterId clusterId; + CommandId commandId; + GroupId groupId; + FabricIndex fabric; + + Credentials::GroupDataProvider::GroupEndpoint mapping; + Credentials::GroupDataProvider * groupDataProvider = Credentials::GetGroupDataProvider(); + Credentials::GroupDataProvider::EndpointIterator * iterator; + + err = aCommandElement.GetPath(&commandPath); + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); + + err = commandPath.GetGroupCommandPath(&clusterId, &commandId); + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); + + VerifyOrDie(mpResponder); + // The optionalGroupId must have a value, otherwise we wouldn't have reached this code path. + groupId = mpResponder->GetGroupId().Value(); + fabric = GetAccessingFabricIndex(); + + ChipLogDetail(DataManagement, "Received group command for Group=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, + groupId, ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId)); + + err = aCommandElement.GetFields(&commandDataReader); + if (CHIP_END_OF_TLV == err) + { + ChipLogDetail(DataManagement, + "Received command without data for Group=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, groupId, + ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId)); + commandDataReader.Init(sNoFields); + err = commandDataReader.Next(); + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::InvalidAction); + } + VerifyOrReturnError(err == CHIP_NO_ERROR, Status::Failure); + + // Per spec, we do the "is this a timed command?" check for every path, but + // since all paths that fail it just get silently discarded we can do it + // once up front and discard all the paths at once. Ordering with respect + // to ACL and command presence checks does not matter, because the behavior + // is the same for all of them: ignore the path. + if (CommandNeedsTimedInvoke(clusterId, commandId)) + { + // Group commands are never timed. + return Status::Success; + } + + // No check for `CommandIsFabricScoped` unlike in `ProcessCommandDataIB()` since group commands + // always have an accessing fabric, by definition. + + // Find which endpoints can process the command, and dispatch to them. + iterator = groupDataProvider->IterateEndpoints(fabric); + VerifyOrReturnError(iterator != nullptr, Status::Failure); + + while (iterator->Next(mapping)) + { + if (groupId != mapping.group_id) + { + continue; + } + + ChipLogDetail(DataManagement, + "Processing group command for Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI, + mapping.endpoint_id, ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId)); + + const ConcreteCommandPath concretePath(mapping.endpoint_id, clusterId, commandId); + + if (mpCallback->CommandExists(concretePath) != Status::Success) + { + ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint 0x%x", + ChipLogValueMEI(commandId), ChipLogValueMEI(clusterId), mapping.endpoint_id); + + continue; + } + + { + Access::SubjectDescriptor subjectDescriptor = GetSubjectDescriptor(); + Access::RequestPath requestPath{ .cluster = concretePath.mClusterId, .endpoint = concretePath.mEndpointId }; + Access::Privilege requestPrivilege = RequiredPrivilege::ForInvokeCommand(concretePath); + err = Access::GetAccessControl().Check(subjectDescriptor, requestPath, requestPrivilege); + if (err != CHIP_NO_ERROR) + { + // NOTE: an expected error is CHIP_ERROR_ACCESS_DENIED, but there could be other unexpected errors; + // therefore, keep processing subsequent commands, and if any errors continue, those subsequent + // commands will likewise fail. + continue; + } + } + if ((err = DataModelCallbacks::GetInstance()->PreCommandReceived(concretePath, GetSubjectDescriptor())) == CHIP_NO_ERROR) + { + TLV::TLVReader dataReader(commandDataReader); + mpCallback->DispatchCommand(*this, concretePath, dataReader); + DataModelCallbacks::GetInstance()->PostCommandReceived(concretePath, GetSubjectDescriptor()); + } + else + { + ChipLogError(DataManagement, + "Error when calling PreCommandReceived for Endpoint=%u Cluster=" ChipLogFormatMEI + " Command=" ChipLogFormatMEI " : %" CHIP_ERROR_FORMAT, + mapping.endpoint_id, ChipLogValueMEI(clusterId), ChipLogValueMEI(commandId), err.Format()); + continue; + } + } + iterator->Release(); + return Status::Success; +} + +CHIP_ERROR CommandHandlerImpl::TryAddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus) +{ + // Return early when response should not be sent out. + VerifyOrReturnValue(ResponsesAccepted(), CHIP_NO_ERROR); + + ReturnErrorOnFailure(PrepareStatus(aCommandPath)); + CommandStatusIB::Builder & commandStatus = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus(); + StatusIB::Builder & statusIBBuilder = commandStatus.CreateErrorStatus(); + ReturnErrorOnFailure(commandStatus.GetError()); + statusIBBuilder.EncodeStatusIB(aStatus); + ReturnErrorOnFailure(statusIBBuilder.GetError()); + return FinishStatus(); +} + +CHIP_ERROR CommandHandlerImpl::AddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus) +{ + return TryAddingResponse([&]() -> CHIP_ERROR { return TryAddStatusInternal(aCommandPath, aStatus); }); +} + +void CommandHandlerImpl::AddStatus(const ConcreteCommandPath & aCommandPath, const Protocols::InteractionModel::Status aStatus, + const char * context) +{ + + CHIP_ERROR error = FallibleAddStatus(aCommandPath, aStatus, context); + + if (error != CHIP_NO_ERROR) + { + ChipLogError(DataManagement, "Failed to add command status: %" CHIP_ERROR_FORMAT, error.Format()); + // TODO(#30453) we could call mpResponder->ResponseDropped() if err == CHIP_ERROR_NO_MEMORY. This should + // be done as a follow up so that change can be evaluated as a standalone PR. + + // Do not crash if the status has not been added due to running out of packet buffers or other resources. + // It is better to drop a single response than to go offline and lose all sessions and subscriptions. + VerifyOrDie(error == CHIP_ERROR_NO_MEMORY); + } +} + +CHIP_ERROR CommandHandlerImpl::FallibleAddStatus(const ConcreteCommandPath & path, const Protocols::InteractionModel::Status status, + const char * context) +{ + if (status != Status::Success) + { + if (context == nullptr) + { + context = "no additional context"; + } + + ChipLogError(DataManagement, + "Endpoint=%u Cluster=" ChipLogFormatMEI " Command=" ChipLogFormatMEI " status " ChipLogFormatIMStatus " (%s)", + path.mEndpointId, ChipLogValueMEI(path.mClusterId), ChipLogValueMEI(path.mCommandId), + ChipLogValueIMStatus(status), context); + } + + return AddStatusInternal(path, StatusIB(status)); +} + +CHIP_ERROR CommandHandlerImpl::AddClusterSpecificSuccess(const ConcreteCommandPath & aCommandPath, ClusterStatus aClusterStatus) +{ + return AddStatusInternal(aCommandPath, StatusIB(Status::Success, aClusterStatus)); +} + +CHIP_ERROR CommandHandlerImpl::AddClusterSpecificFailure(const ConcreteCommandPath & aCommandPath, ClusterStatus aClusterStatus) +{ + return AddStatusInternal(aCommandPath, StatusIB(Status::Failure, aClusterStatus)); +} + +CHIP_ERROR CommandHandlerImpl::PrepareInvokeResponseCommand(const ConcreteCommandPath & aResponseCommandPath, + const CommandHandlerImpl::InvokeResponseParameters & aPrepareParameters) +{ + auto commandPathRegistryEntry = GetCommandPathRegistry().Find(aPrepareParameters.mRequestCommandPath); + VerifyOrReturnValue(commandPathRegistryEntry.has_value(), CHIP_ERROR_INCORRECT_STATE); + + return PrepareInvokeResponseCommand(*commandPathRegistryEntry, aResponseCommandPath, aPrepareParameters.mStartOrEndDataStruct); +} + +CHIP_ERROR CommandHandlerImpl::PrepareInvokeResponseCommand(const CommandPathRegistryEntry & apCommandPathRegistryEntry, + const ConcreteCommandPath & aCommandPath, bool aStartDataStruct) +{ + // Intentionally omitting the ResponsesAccepted early exit. Direct use of PrepareInvokeResponseCommand + // is discouraged, as it often indicates incorrect usage patterns (see GitHub issue #32486). + // If you're encountering CHIP_ERROR_INCORRECT_STATE, refactoring to use AddResponse is recommended. + ReturnErrorOnFailure(AllocateBuffer()); + + if (!mInternalCallToAddResponseData && mState == State::AddedCommand) + { + // An attempt is being made to add CommandData InvokeResponse using primitive + // CommandHandlerImpl APIs. While not recommended, as this potentially leaves the + // CommandHandlerImpl in an incorrect state upon failure, this approach is permitted + // for legacy reasons. To maximize the likelihood of success, particularly when + // handling large amounts of data, we try to obtain a new, completely empty + // InvokeResponseMessage, as the existing one already has space occupied. + ReturnErrorOnFailure(FinalizeInvokeResponseMessageAndPrepareNext()); + } + + CreateBackupForResponseRollback(); + // + // We must not be in the middle of preparing a command, or having prepared or sent one. + // + VerifyOrReturnError(mState == State::NewResponseMessage || mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE); + + // TODO(#30453): See if we can pass this back up the stack so caller can provide this instead of taking up + // space in CommandHanlder. + mRefForResponse = apCommandPathRegistryEntry.ref; + + MoveToState(State::Preparing); + InvokeResponseIBs::Builder & invokeResponses = mInvokeResponseBuilder.GetInvokeResponses(); + InvokeResponseIB::Builder & invokeResponse = invokeResponses.CreateInvokeResponse(); + ReturnErrorOnFailure(invokeResponses.GetError()); + + CommandDataIB::Builder & commandData = invokeResponse.CreateCommand(); + ReturnErrorOnFailure(commandData.GetError()); + CommandPathIB::Builder & path = commandData.CreatePath(); + ReturnErrorOnFailure(commandData.GetError()); + ReturnErrorOnFailure(path.Encode(aCommandPath)); + if (aStartDataStruct) + { + ReturnErrorOnFailure(commandData.GetWriter()->StartContainer(TLV::ContextTag(CommandDataIB::Tag::kFields), + TLV::kTLVType_Structure, mDataElementContainerType)); + } + MoveToState(State::AddingCommand); + return CHIP_NO_ERROR; +} + +CHIP_ERROR CommandHandlerImpl::FinishCommand(bool aStartDataStruct) +{ + // Intentionally omitting the ResponsesAccepted early exit. Direct use of FinishCommand + // is discouraged, as it often indicates incorrect usage patterns (see GitHub issue #32486). + // If you're encountering CHIP_ERROR_INCORRECT_STATE, refactoring to use AddResponse is recommended. + VerifyOrReturnError(mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE); + CommandDataIB::Builder & commandData = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetCommand(); + if (aStartDataStruct) + { + ReturnErrorOnFailure(commandData.GetWriter()->EndContainer(mDataElementContainerType)); + } + + if (mRefForResponse.has_value()) + { + ReturnErrorOnFailure(commandData.Ref(*mRefForResponse)); + } + + ReturnErrorOnFailure(commandData.EndOfCommandDataIB()); + ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().EndOfInvokeResponseIB()); + MoveToState(State::AddedCommand); + return CHIP_NO_ERROR; +} + +CHIP_ERROR CommandHandlerImpl::PrepareStatus(const ConcreteCommandPath & aCommandPath) +{ + ReturnErrorOnFailure(AllocateBuffer()); + // + // We must not be in the middle of preparing a command, or having prepared or sent one. + // + VerifyOrReturnError(mState == State::NewResponseMessage || mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE); + if (mState == State::AddedCommand) + { + CreateBackupForResponseRollback(); + } + + auto commandPathRegistryEntry = GetCommandPathRegistry().Find(aCommandPath); + VerifyOrReturnError(commandPathRegistryEntry.has_value(), CHIP_ERROR_INCORRECT_STATE); + mRefForResponse = commandPathRegistryEntry->ref; + + MoveToState(State::Preparing); + InvokeResponseIBs::Builder & invokeResponses = mInvokeResponseBuilder.GetInvokeResponses(); + InvokeResponseIB::Builder & invokeResponse = invokeResponses.CreateInvokeResponse(); + ReturnErrorOnFailure(invokeResponses.GetError()); + CommandStatusIB::Builder & commandStatus = invokeResponse.CreateStatus(); + ReturnErrorOnFailure(commandStatus.GetError()); + CommandPathIB::Builder & path = commandStatus.CreatePath(); + ReturnErrorOnFailure(commandStatus.GetError()); + ReturnErrorOnFailure(path.Encode(aCommandPath)); + MoveToState(State::AddingCommand); + return CHIP_NO_ERROR; +} + +CHIP_ERROR CommandHandlerImpl::FinishStatus() +{ + VerifyOrReturnError(mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE); + + CommandStatusIB::Builder & commandStatus = mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus(); + if (mRefForResponse.has_value()) + { + ReturnErrorOnFailure(commandStatus.Ref(*mRefForResponse)); + } + + ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetStatus().EndOfCommandStatusIB()); + ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().EndOfInvokeResponseIB()); + MoveToState(State::AddedCommand); + return CHIP_NO_ERROR; +} + +void CommandHandlerImpl::CreateBackupForResponseRollback() +{ + VerifyOrReturn(mState == State::NewResponseMessage || mState == State::AddedCommand); + VerifyOrReturn(mInvokeResponseBuilder.GetInvokeResponses().GetError() == CHIP_NO_ERROR); + VerifyOrReturn(mInvokeResponseBuilder.GetError() == CHIP_NO_ERROR); + mInvokeResponseBuilder.Checkpoint(mBackupWriter); + mBackupState = mState; + mRollbackBackupValid = true; +} + +CHIP_ERROR CommandHandlerImpl::RollbackResponse() +{ + VerifyOrReturnError(mRollbackBackupValid, CHIP_ERROR_INCORRECT_STATE); + VerifyOrReturnError(mState == State::Preparing || mState == State::AddingCommand, CHIP_ERROR_INCORRECT_STATE); + ChipLogDetail(DataManagement, "Rolling back response"); + // TODO(#30453): Rollback of mInvokeResponseBuilder should handle resetting + // InvokeResponses. + mInvokeResponseBuilder.GetInvokeResponses().ResetError(); + mInvokeResponseBuilder.Rollback(mBackupWriter); + MoveToState(mBackupState); + mRollbackBackupValid = false; + return CHIP_NO_ERROR; +} + +TLV::TLVWriter * CommandHandlerImpl::GetCommandDataIBTLVWriter() +{ + if (mState != State::AddingCommand) + { + return nullptr; + } + + return mInvokeResponseBuilder.GetInvokeResponses().GetInvokeResponse().GetCommand().GetWriter(); +} + +FabricIndex CommandHandlerImpl::GetAccessingFabricIndex() const +{ + VerifyOrDie(!mGoneAsync); + VerifyOrDie(mpResponder); + return mpResponder->GetAccessingFabricIndex(); +} + +CHIP_ERROR CommandHandlerImpl::FinalizeInvokeResponseMessageAndPrepareNext() +{ + ReturnErrorOnFailure(FinalizeInvokeResponseMessage(/* aHasMoreChunks = */ true)); + // After successfully finalizing InvokeResponseMessage, no buffer should remain + // allocated. + VerifyOrDie(!mBufferAllocated); + CHIP_ERROR err = AllocateBuffer(); + if (err != CHIP_NO_ERROR) + { + // TODO(#30453): Improve ResponseDropped calls to occur only when dropping is + // definitively guaranteed. + // Response dropping is not yet definitive as a subsequent call + // to AllocateBuffer might succeed. + VerifyOrDie(mpResponder); + mpResponder->ResponseDropped(); + } + return err; +} + +CHIP_ERROR CommandHandlerImpl::FinalizeInvokeResponseMessage(bool aHasMoreChunks) +{ + System::PacketBufferHandle packet; + + VerifyOrReturnError(mState == State::AddedCommand, CHIP_ERROR_INCORRECT_STATE); + ReturnErrorOnFailure(mInvokeResponseBuilder.GetInvokeResponses().EndOfInvokeResponses()); + if (aHasMoreChunks) + { + // Unreserving space previously reserved for MoreChunkedMessages is done + // in the call to mInvokeResponseBuilder.MoreChunkedMessages. + mInvokeResponseBuilder.MoreChunkedMessages(aHasMoreChunks); + ReturnErrorOnFailure(mInvokeResponseBuilder.GetError()); + } + ReturnErrorOnFailure(mInvokeResponseBuilder.EndOfInvokeResponseMessage()); + ReturnErrorOnFailure(mCommandMessageWriter.Finalize(&packet)); + VerifyOrDie(mpResponder); + mpResponder->AddInvokeResponseToSend(std::move(packet)); + mBufferAllocated = false; + mRollbackBackupValid = false; + return CHIP_NO_ERROR; +} + +void CommandHandlerImpl::SetExchangeInterface(CommandHandlerExchangeInterface * commandResponder) +{ + VerifyOrDieWithMsg(mState == State::Idle, DataManagement, "CommandResponseSender can only be set in idle state"); + mpResponder = commandResponder; +} + +const char * CommandHandlerImpl::GetStateStr() const +{ +#if CHIP_DETAIL_LOGGING + switch (mState) + { + case State::Idle: + return "Idle"; + + case State::NewResponseMessage: + return "NewResponseMessage"; + + case State::Preparing: + return "Preparing"; + + case State::AddingCommand: + return "AddingCommand"; + + case State::AddedCommand: + return "AddedCommand"; + + case State::DispatchResponses: + return "DispatchResponses"; + + case State::AwaitingDestruction: + return "AwaitingDestruction"; + } +#endif // CHIP_DETAIL_LOGGING + return "N/A"; +} + +void CommandHandlerImpl::MoveToState(const State aTargetState) +{ + mState = aTargetState; + ChipLogDetail(DataManagement, "Command handler moving to [%10.10s]", GetStateStr()); +} + +void CommandHandlerImpl::FlushAcksRightAwayOnSlowCommand() +{ + if (mpResponder) + { + mpResponder->HandlingSlowCommand(); + } +} + +Access::SubjectDescriptor CommandHandlerImpl::GetSubjectDescriptor() const +{ + VerifyOrDie(!mGoneAsync); + VerifyOrDie(mpResponder); + return mpResponder->GetSubjectDescriptor(); +} + +bool CommandHandlerImpl::IsTimedInvoke() const +{ + return mTimedRequest; +} + +void CommandHandlerImpl::AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) +{ + CHIP_ERROR err = AddResponseData(aRequestCommandPath, aResponseCommandId, aEncodable); + if (err != CHIP_NO_ERROR) + { + ChipLogError(DataManagement, "Adding response failed: %" CHIP_ERROR_FORMAT ". Returning failure instead.", err.Format()); + AddStatus(aRequestCommandPath, Protocols::InteractionModel::Status::Failure); + } +} + +Messaging::ExchangeContext * CommandHandlerImpl::GetExchangeContext() const +{ + VerifyOrDie(mpResponder); + return mpResponder->GetExchangeContext(); +} + +#if CHIP_WITH_NLFAULTINJECTION + +namespace { + +CHIP_ERROR TestOnlyExtractCommandPathFromNextInvokeRequest(TLV::TLVReader & invokeRequestsReader, + ConcreteCommandPath & concretePath) +{ + ReturnErrorOnFailure(invokeRequestsReader.Next(TLV::AnonymousTag())); + CommandDataIB::Parser commandData; + ReturnErrorOnFailure(commandData.Init(invokeRequestsReader)); + CommandPathIB::Parser commandPath; + ReturnErrorOnFailure(commandData.GetPath(&commandPath)); + return commandPath.GetConcreteCommandPath(concretePath); +} + +[[maybe_unused]] const char * GetFaultInjectionTypeStr(CommandHandlerImpl::NlFaultInjectionType faultType) +{ + switch (faultType) + { + case CommandHandlerImpl::NlFaultInjectionType::SeparateResponseMessages: + return "Each response will be sent in a separate InvokeResponseMessage. The order of responses will be the same as the " + "original request."; + case CommandHandlerImpl::NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder: + return "Each response will be sent in a separate InvokeResponseMessage. The order of responses will be reversed from the " + "original request."; + case CommandHandlerImpl::NlFaultInjectionType::SkipSecondResponse: + return "Single InvokeResponseMessages. Dropping response to second request"; + } + ChipLogError(DataManagement, "TH Failure: Unexpected fault type"); + chipAbort(); +} + +} // anonymous namespace + +// This method intentionally duplicates code from other sections. While code consolidation +// is generally preferred, here we prioritize generating a clear crash message to aid in +// troubleshooting test failures. +void CommandHandlerImpl::TestOnlyInvokeCommandRequestWithFaultsInjected(CommandHandlerExchangeInterface & commandResponder, + System::PacketBufferHandle && payload, bool isTimedInvoke, + NlFaultInjectionType faultType) +{ + VerifyOrDieWithMsg(mState == State::Idle, DataManagement, "TH Failure: state should be Idle, issue with TH"); + SetExchangeInterface(&commandResponder); + + ChipLogProgress(DataManagement, "Response to InvokeRequestMessage overridden by fault injection"); + ChipLogProgress(DataManagement, " Injecting the following response:%s", GetFaultInjectionTypeStr(faultType)); + + Handle workHandle(this); + VerifyOrDieWithMsg(!commandResponder.GetGroupId().HasValue(), DataManagement, "DUT Failure: Unexpected Group Command"); + + System::PacketBufferTLVReader reader; + InvokeRequestMessage::Parser invokeRequestMessage; + InvokeRequests::Parser invokeRequests; + reader.Init(std::move(payload)); + VerifyOrDieWithMsg(invokeRequestMessage.Init(reader) == CHIP_NO_ERROR, DataManagement, + "TH Failure: Failed 'invokeRequestMessage.Init(reader)'"); +#if CHIP_CONFIG_IM_PRETTY_PRINT + invokeRequestMessage.PrettyPrint(); +#endif + + VerifyOrDieWithMsg(invokeRequestMessage.GetSuppressResponse(&mSuppressResponse) == CHIP_NO_ERROR, DataManagement, + "DUT Failure: Mandatory SuppressResponse field missing"); + VerifyOrDieWithMsg(invokeRequestMessage.GetTimedRequest(&mTimedRequest) == CHIP_NO_ERROR, DataManagement, + "DUT Failure: Mandatory TimedRequest field missing"); + VerifyOrDieWithMsg(invokeRequestMessage.GetInvokeRequests(&invokeRequests) == CHIP_NO_ERROR, DataManagement, + "DUT Failure: Mandatory InvokeRequests field missing"); + VerifyOrDieWithMsg(mTimedRequest == isTimedInvoke, DataManagement, + "DUT Failure: TimedRequest value in message mismatches action"); + + { + InvokeRequestMessage::Parser validationInvokeRequestMessage = invokeRequestMessage; + VerifyOrDieWithMsg(ValidateInvokeRequestMessageAndBuildRegistry(validationInvokeRequestMessage) == CHIP_NO_ERROR, + DataManagement, "DUT Failure: InvokeRequestMessage contents were invalid"); + } + + TLV::TLVReader invokeRequestsReader; + invokeRequests.GetReader(&invokeRequestsReader); + + size_t commandCount = 0; + VerifyOrDieWithMsg(TLV::Utilities::Count(invokeRequestsReader, commandCount, false /* recurse */) == CHIP_NO_ERROR, + DataManagement, + "TH Failure: Failed to get the length of InvokeRequests after InvokeRequestMessage validation"); + + // The command count check (specifically for a count of 2) is tied to IDM_1_3. This may need adjustment for + // compatibility with future test plans. + VerifyOrDieWithMsg(commandCount == 2, DataManagement, "DUT failure: We were strictly expecting exactly 2 InvokeRequests"); + mReserveSpaceForMoreChunkMessages = true; + + { + // Response path is the same as request path since we are replying with a failure message. + ConcreteCommandPath concreteResponsePath1; + ConcreteCommandPath concreteResponsePath2; + VerifyOrDieWithMsg( + TestOnlyExtractCommandPathFromNextInvokeRequest(invokeRequestsReader, concreteResponsePath1) == CHIP_NO_ERROR, + DataManagement, "DUT Failure: Issues encountered while extracting the ConcreteCommandPath from the first request"); + VerifyOrDieWithMsg( + TestOnlyExtractCommandPathFromNextInvokeRequest(invokeRequestsReader, concreteResponsePath2) == CHIP_NO_ERROR, + DataManagement, "DUT Failure: Issues encountered while extracting the ConcreteCommandPath from the second request"); + + if (faultType == NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder) + { + ConcreteCommandPath temp(concreteResponsePath1); + concreteResponsePath1 = concreteResponsePath2; + concreteResponsePath2 = temp; + } + + VerifyOrDieWithMsg(FallibleAddStatus(concreteResponsePath1, Status::Failure) == CHIP_NO_ERROR, DataManagement, + "TH Failure: Error adding the first InvokeResponse"); + if (faultType == NlFaultInjectionType::SeparateResponseMessages || + faultType == NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder) + { + VerifyOrDieWithMsg(FinalizeInvokeResponseMessageAndPrepareNext() == CHIP_NO_ERROR, DataManagement, + "TH Failure: Failed to create second InvokeResponseMessage"); + } + if (faultType != NlFaultInjectionType::SkipSecondResponse) + { + VerifyOrDieWithMsg(FallibleAddStatus(concreteResponsePath2, Status::Failure) == CHIP_NO_ERROR, DataManagement, + "TH Failure: Error adding the second InvokeResponse"); + } + } + + VerifyOrDieWithMsg(invokeRequestsReader.Next() == CHIP_END_OF_TLV, DataManagement, + "DUT Failure: Unexpected TLV ending of InvokeRequests"); + VerifyOrDieWithMsg(invokeRequestMessage.ExitContainer() == CHIP_NO_ERROR, DataManagement, + "DUT Failure: InvokeRequestMessage TLV is not properly terminated"); +} +#endif // CHIP_WITH_NLFAULTINJECTION + +} // namespace app +} // namespace chip diff --git a/src/app/CommandHandlerImpl.h b/src/app/CommandHandlerImpl.h new file mode 100644 index 00000000000000..fd85f35440f7cc --- /dev/null +++ b/src/app/CommandHandlerImpl.h @@ -0,0 +1,476 @@ +/* + * Copyright (c) 2020-2024 Project CHIP Authors + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace chip { +namespace app { + +class CommandHandlerImpl : public CommandHandler +{ +public: + class Callback + { + public: + virtual ~Callback() = default; + + /* + * Method that signals to a registered callback that this object + * has completed doing useful work and is now safe for release/destruction. + */ + virtual void OnDone(CommandHandlerImpl & apCommandObj) = 0; + + /* + * Upon processing of a CommandDataIB, this method is invoked to dispatch the command + * to the right server-side handler provided by the application. + */ + virtual void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, + TLV::TLVReader & apPayload) = 0; + + /* + * Check to see if a command implementation exists for a specific + * concrete command path. If it does, Success will be returned. If + * not, one of UnsupportedEndpoint, UnsupportedCluster, or + * UnsupportedCommand will be returned, depending on how the command + * fails to exist. + */ + virtual Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) = 0; + }; + + struct InvokeResponseParameters + { + InvokeResponseParameters(const ConcreteCommandPath & aRequestCommandPath) : mRequestCommandPath(aRequestCommandPath) {} + + InvokeResponseParameters & SetStartOrEndDataStruct(bool aStartOrEndDataStruct) + { + mStartOrEndDataStruct = aStartOrEndDataStruct; + return *this; + } + + ConcreteCommandPath mRequestCommandPath; + /** + * Whether the method this is being provided to should start/end the TLV container for the CommandFields element + * within CommandDataIB. + */ + bool mStartOrEndDataStruct = true; + }; + + struct TestOnlyOverrides + { + public: + CommandPathRegistry * commandPathRegistry = nullptr; + CommandHandlerExchangeInterface * commandResponder = nullptr; + }; + + /* + * The callback passed in has to outlive this CommandHandler object. + */ + CommandHandlerImpl(Callback * apCallback); + + /* + * The destructor will also invalidate all Handles created for this CommandHandlerImpl. + */ + virtual ~CommandHandlerImpl(); + + /* + * Constructor to override the number of supported paths per invoke and command responder. + * + * The callback and any pointers passed via TestOnlyOverrides must outlive this + * CommandHandlerImpl object. + * + * For testing purposes. + */ + CommandHandlerImpl(TestOnlyOverrides & aTestOverride, Callback * apCallback); + + /**************** CommandHandler interface implementation ***********************/ + + using CommandHandler::AddResponseData; + + void FlushAcksRightAwayOnSlowCommand() override; + + CHIP_ERROR FallibleAddStatus(const ConcreteCommandPath & aRequestCommandPath, const Protocols::InteractionModel::Status aStatus, + const char * context = nullptr) override; + void AddStatus(const ConcreteCommandPath & aCommandPath, const Protocols::InteractionModel::Status aStatus, + const char * context = nullptr) override; + CHIP_ERROR AddClusterSpecificSuccess(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) override; + CHIP_ERROR AddClusterSpecificFailure(const ConcreteCommandPath & aRequestCommandPath, ClusterStatus aClusterStatus) override; + + CHIP_ERROR AddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) override; + void AddResponse(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable) override; + + Access::SubjectDescriptor GetSubjectDescriptor() const override; + FabricIndex GetAccessingFabricIndex() const override; + bool IsTimedInvoke() const override; + Messaging::ExchangeContext * GetExchangeContext() const override; + + /**************** Implementation-specific logic ***********************/ + + /* + * Main entrypoint for this class to handle an InvokeRequestMessage. + * + * This function MAY call the registered OnDone callback before returning. + * To prevent immediate OnDone invocation, callers can wrap their CommandHandlerImpl instance + * within a CommandHandler::Handle. + * + * isTimedInvoke is true if and only if this is part of a Timed Invoke + * transaction (i.e. was preceded by a Timed Request). If we reach here, + * the timer verification has already been done. + * + * commandResponder handles sending InvokeResponses, added by clusters, to the client. The + * command responder object must outlive this CommandHandler object. It is only safe to + * release after the caller of OnInvokeCommandRequest receives the OnDone callback. + */ + Protocols::InteractionModel::Status OnInvokeCommandRequest(CommandHandlerExchangeInterface & commandResponder, + System::PacketBufferHandle && payload, bool isTimedInvoke); + + /** + * Checks that all CommandDataIB within InvokeRequests satisfy the spec's general + * constraints for CommandDataIB. Additionally checks that InvokeRequestMessage is + * properly formatted. + * + * This also builds a registry to ensure that all commands can be responded + * to with the data required as per spec. + */ + CHIP_ERROR ValidateInvokeRequestMessageAndBuildRegistry(InvokeRequestMessage::Parser & invokeRequestMessage); + + /** + * This adds a new CommandDataIB element into InvokeResponses for the associated + * aRequestCommandPath. This adds up until the `CommandFields` element within + * `CommandDataIB`. + * + * This call will fail if CommandHandler is already in the middle of building a + * CommandStatusIB or CommandDataIB (i.e. something has called Prepare*, without + * calling Finish*), or is already sending InvokeResponseMessage. + * + * Upon success, the caller is expected to call `FinishCommand` once they have added + * all the fields into the CommandFields element of CommandDataIB. + * + * @param [in] aResponseCommandPath the concrete response path that we are sending to Requester. + * @param [in] aPrepareParameters struct containing paramters needs for preparing a command. Data + * such as request path, and whether this method should start the CommandFields element within + * CommandDataIB. + */ + CHIP_ERROR PrepareInvokeResponseCommand(const ConcreteCommandPath & aResponseCommandPath, + const InvokeResponseParameters & aPrepareParameters); + + /** + * Finishes the CommandDataIB element within the InvokeResponses. + * + * Caller must have first successfully called `PrepareInvokeResponseCommand`. + * + * @param [in] aEndDataStruct end the TLV container for the CommandFields element within + * CommandDataIB. This should match the boolean passed into Prepare*. + * + * @return CHIP_ERROR_INCORRECT_STATE + * If device has not previously successfully called + * `PrepareInvokeResponseCommand`. + * @return CHIP_ERROR_BUFFER_TOO_SMALL + * If writing the values needed to finish the InvokeReponseIB + * with the current contents of the InvokeResponseMessage + * would exceed the limit. When this error occurs, it is possible + * we have already closed some of the IB Builders that were + * previously started in `PrepareInvokeResponseCommand`. + * @return CHIP_ERROR_NO_MEMORY + * If TLVWriter attempted to allocate an output buffer failed due to + * lack of memory. + * @return other Other TLVWriter related errors. Typically occurs if + * `GetCommandDataIBTLVWriter()` was called and used incorrectly. + */ + // TODO(#30453): We should be able to eliminate the chances of OOM issues with reserve. + // This will be completed in a follow up PR. + CHIP_ERROR FinishCommand(bool aEndDataStruct = true); + + TLV::TLVWriter * GetCommandDataIBTLVWriter(); + +#if CHIP_WITH_NLFAULTINJECTION + + enum class NlFaultInjectionType : uint8_t + { + SeparateResponseMessages, + SeparateResponseMessagesAndInvertedResponseOrder, + SkipSecondResponse + }; + + /** + * @brief Sends InvokeResponseMessages with injected faults for certification testing. + * + * The Test Harness (TH) uses this to simulate various server response behaviors, + * ensuring the Device Under Test (DUT) handles responses per specification. + * + * This function strictly validates the DUT's InvokeRequestMessage against the test plan. + * If deviations occur, the TH terminates with a detailed error message. + * + * @param commandResponder commandResponder that will send the InvokeResponseMessages to the client. + * @param payload Payload of the incoming InvokeRequestMessage from the client. + * @param isTimedInvoke Indicates whether the interaction is timed. + * @param faultType The specific type of fault to inject into the response. + */ + // TODO(#30453): After refactoring CommandHandler for better unit testability, create a + // unit test specifically for the fault injection behavior. + void TestOnlyInvokeCommandRequestWithFaultsInjected(CommandHandlerExchangeInterface & commandResponder, + System::PacketBufferHandle && payload, bool isTimedInvoke, + NlFaultInjectionType faultType); +#endif // CHIP_WITH_NLFAULTINJECTION + +protected: + // Lifetime management for CommandHandler::Handle + + void IncrementHoldOff(Handle * apHandle) override; + void DecrementHoldOff(Handle * apHandle) override; + +private: + friend class TestCommandInteraction; + friend class CommandHandler::Handle; + + enum class State : uint8_t + { + Idle, ///< Default state that the object starts out in, where no work has commenced + NewResponseMessage, ///< mInvokeResponseBuilder is ready, with no responses added. + Preparing, ///< We are prepaing the command or status header. + AddingCommand, ///< In the process of adding a command. + AddedCommand, ///< A command has been completely encoded and is awaiting transmission. + DispatchResponses, ///< The command response(s) are being dispatched. + AwaitingDestruction, ///< The object has completed its work and is awaiting destruction by the application. + }; + + /** + * @brief Best effort to add InvokeResponse to InvokeResponseMessage. + * + * Tries to add response using lambda. Upon failure to add response, attempts + * to rollback the InvokeResponseMessage to a known good state. If failure is due + * to insufficient space in the current InvokeResponseMessage: + * - Finalizes the current InvokeResponseMessage. + * - Allocates a new InvokeResponseMessage. + * - Reattempts to add the InvokeResponse to the new InvokeResponseMessage. + * + * @param [in] addResponseFunction A lambda function responsible for adding the + * response to the current InvokeResponseMessage. + */ + template + CHIP_ERROR TryAddingResponse(Function && addResponseFunction) + { + // Invalidate any existing rollback backups. The addResponseFunction is + // expected to create a new backup during either PrepareInvokeResponseCommand + // or PrepareStatus execution. Direct invocation of + // CreateBackupForResponseRollback is avoided since the buffer used by + // InvokeResponseMessage might not be allocated until a Prepare* function + // is called. + mRollbackBackupValid = false; + CHIP_ERROR err = addResponseFunction(); + if (err == CHIP_NO_ERROR) + { + return CHIP_NO_ERROR; + } + ReturnErrorOnFailure(RollbackResponse()); + // If we failed to add a command due to lack of space in the + // packet, we will make another attempt to add the response using + // an additional InvokeResponseMessage. + if (mState != State::AddedCommand || err != CHIP_ERROR_NO_MEMORY) + { + return err; + } + ReturnErrorOnFailure(FinalizeInvokeResponseMessageAndPrepareNext()); + err = addResponseFunction(); + if (err != CHIP_NO_ERROR) + { + // The return value of RollbackResponse is ignored, as we prioritize + // conveying the error generated by addResponseFunction to the + // caller. + RollbackResponse(); + } + return err; + } + + void MoveToState(const State aTargetState); + const char * GetStateStr() const; + + /** + * Create a backup to enable rolling back to the state prior to ResponseData encoding in the event of failure. + */ + void CreateBackupForResponseRollback(); + + /** + * Rollback the state to before encoding the current ResponseData (before calling PrepareInvokeResponseCommand / PrepareStatus) + * + * Requires CreateBackupForResponseRollback to be called at the start of PrepareInvokeResponseCommand / PrepareStatus + */ + CHIP_ERROR RollbackResponse(); + + /* + * This forcibly closes the exchange context if a valid one is pointed to. Such a situation does + * not arise during normal message processing flows that all normally call Close() above. This can only + * arise due to application-initiated destruction of the object when this object is handling receiving/sending + * message payloads. + */ + void Abort(); + + /* + * Allocates a packet buffer used for encoding an invoke response payload. + * + * This can be called multiple times safely, as it will only allocate the buffer once for the lifetime + * of this object. + */ + CHIP_ERROR AllocateBuffer(); + + /** + * This will add a new CommandStatusIB element into InvokeResponses. It will put the + * aCommandPath into the CommandPath element within CommandStatusIB. + * + * This call will fail if CommandHandler is already in the middle of building a + * CommandStatusIB or CommandDataIB (i.e. something has called Prepare*, without + * calling Finish*), or is already sending InvokeResponseMessage. + * + * Upon success, the caller is expected to call `FinishStatus` once they have encoded + * StatusIB. + * + * @param [in] aCommandPath the concrete path of the command we are responding to. + */ + CHIP_ERROR PrepareStatus(const ConcreteCommandPath & aCommandPath); + + /** + * Finishes the CommandStatusIB element within the InvokeResponses. + * + * Caller must have first successfully called `PrepareStatus`. + */ + CHIP_ERROR FinishStatus(); + + CHIP_ERROR PrepareInvokeResponseCommand(const CommandPathRegistryEntry & apCommandPathRegistryEntry, + const ConcreteCommandPath & aCommandPath, bool aStartDataStruct); + + CHIP_ERROR FinalizeLastInvokeResponseMessage() { return FinalizeInvokeResponseMessage(/* aHasMoreChunks = */ false); } + + CHIP_ERROR FinalizeInvokeResponseMessageAndPrepareNext(); + + CHIP_ERROR FinalizeInvokeResponseMessage(bool aHasMoreChunks); + + Protocols::InteractionModel::Status ProcessInvokeRequest(System::PacketBufferHandle && payload, bool isTimedInvoke); + + /** + * Called internally to signal the completion of all work on this object, gracefully close the + * exchange (by calling into the base class) and finally, signal to a registerd callback that it's + * safe to release this object. + */ + void Close(); + + /** + * ProcessCommandDataIB is only called when a unicast invoke command request is received + * It requires the endpointId in its command path to be able to dispatch the command + */ + Protocols::InteractionModel::Status ProcessCommandDataIB(CommandDataIB::Parser & aCommandElement); + + /** + * ProcessGroupCommandDataIB is only called when a group invoke command request is received + * It doesn't need the endpointId in it's command path since it uses the GroupId in message metadata to find it + */ + Protocols::InteractionModel::Status ProcessGroupCommandDataIB(CommandDataIB::Parser & aCommandElement); + + CHIP_ERROR TryAddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus); + + CHIP_ERROR AddStatusInternal(const ConcreteCommandPath & aCommandPath, const StatusIB & aStatus); + + /** + * If this function fails, it may leave our TLV buffer in an inconsistent state. + * Callers should snapshot as needed before calling this function, and roll back + * as needed afterward. + * + * @param [in] aRequestCommandPath the concrete path of the command we are responding to + * @param [in] aResponseCommandId the id of the command to encode + * @param [in] aEncodable the data to encode for the given aResponseCommandId + */ + CHIP_ERROR TryAddResponseData(const ConcreteCommandPath & aRequestCommandPath, CommandId aResponseCommandId, + DataModel::EncodableToTLV & aEncodable); + + void SetExchangeInterface(CommandHandlerExchangeInterface * commandResponder); + + /** + * Check whether the InvokeRequest we are handling is targeted to a group. + */ + bool IsGroupRequest() { return mGroupRequest; } + + bool ResponsesAccepted() { return !(mGroupRequest || mpResponder == nullptr); } + + /** + * Sets the state flag to keep the information that request we are handling is targeted to a group. + */ + void SetGroupRequest(bool isGroupRequest) { mGroupRequest = isGroupRequest; } + + CommandPathRegistry & GetCommandPathRegistry() const { return *mCommandPathRegistry; } + + size_t MaxPathsPerInvoke() const { return mMaxPathsPerInvoke; } + + void AddToHandleList(Handle * handle); + + void RemoveFromHandleList(Handle * handle); + + void InvalidateHandles(); + + bool TestOnlyIsInIdleState() const { return mState == State::Idle; } + + Callback * mpCallback = nullptr; + InvokeResponseMessage::Builder mInvokeResponseBuilder; + TLV::TLVType mDataElementContainerType = TLV::kTLVType_NotSpecified; + size_t mPendingWork = 0; + /* List to store all currently-outstanding Handles for this Command Handler.*/ + IntrusiveList mpHandleList; + + chip::System::PacketBufferTLVWriter mCommandMessageWriter; + TLV::TLVWriter mBackupWriter; + size_t mMaxPathsPerInvoke = CHIP_CONFIG_MAX_PATHS_PER_INVOKE; + // TODO(#30453): See if we can reduce this size for the default cases + // TODO Allow flexibility in registration. + BasicCommandPathRegistry mBasicCommandPathRegistry; + CommandPathRegistry * mCommandPathRegistry = &mBasicCommandPathRegistry; + std::optional mRefForResponse; + + CommandHandlerExchangeInterface * mpResponder = nullptr; + + State mState = State::Idle; + State mBackupState; + ScopedChangeOnly mInternalCallToAddResponseData{ false }; + bool mSuppressResponse = false; + bool mTimedRequest = false; + bool mGroupRequest = false; + bool mBufferAllocated = false; + bool mReserveSpaceForMoreChunkMessages = false; + // TODO(#32486): We should introduce breaking change where calls to add CommandData + // need to use AddResponse, and not CommandHandler primitives directly using + // GetCommandDataIBTLVWriter. + bool mRollbackBackupValid = false; + // If mGoneAsync is true, we have finished out initial processing of the + // incoming invoke. After this point, our session could go away at any + // time. + bool mGoneAsync = false; +}; +} // namespace app +} // namespace chip diff --git a/src/app/CommandResponseSender.cpp b/src/app/CommandResponseSender.cpp index d7f40caa476c8e..430fa5f0fa523b 100644 --- a/src/app/CommandResponseSender.cpp +++ b/src/app/CommandResponseSender.cpp @@ -113,7 +113,7 @@ void CommandResponseSender::StartSendingCommandResponses() } } -void CommandResponseSender::OnDone(CommandHandler & apCommandObj) +void CommandResponseSender::OnDone(CommandHandlerImpl & apCommandObj) { if (mState == State::ErrorSentDelayCloseUntilOnDone) { @@ -125,7 +125,7 @@ void CommandResponseSender::OnDone(CommandHandler & apCommandObj) StartSendingCommandResponses(); } -void CommandResponseSender::DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, +void CommandResponseSender::DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) { VerifyOrReturn(mpCommandHandlerCallback); @@ -231,7 +231,7 @@ void CommandResponseSender::OnInvokeCommandRequest(Messaging::ExchangeContext * void CommandResponseSender::TestOnlyInvokeCommandRequestWithFaultsInjected(Messaging::ExchangeContext * ec, System::PacketBufferHandle && payload, bool isTimedInvoke, - CommandHandler::NlFaultInjectionType faultType) + CommandHandlerImpl::NlFaultInjectionType faultType) { VerifyOrDieWithMsg(ec != nullptr, DataManagement, "TH Failure: Incoming exchange context should not be null"); VerifyOrDieWithMsg(mState == State::ReadyForInvokeResponses, DataManagement, diff --git a/src/app/CommandResponseSender.h b/src/app/CommandResponseSender.h index a89970ddec6c92..1925f01b54bf8c 100644 --- a/src/app/CommandResponseSender.h +++ b/src/app/CommandResponseSender.h @@ -17,8 +17,8 @@ #pragma once -#include #include +#include #include #include #include @@ -35,7 +35,7 @@ namespace app { * CommandHandlerExchangeInterface implementation to enable sending InvokeResponseMessage(s). */ class CommandResponseSender : public Messaging::ExchangeDelegate, - public CommandHandler::Callback, + public CommandHandlerImpl::Callback, public CommandHandlerExchangeInterface { public: @@ -50,7 +50,7 @@ class CommandResponseSender : public Messaging::ExchangeDelegate, virtual void OnDone(CommandResponseSender & apResponderObj) = 0; }; - CommandResponseSender(Callback * apCallback, CommandHandler::Callback * apDispatchCallback) : + CommandResponseSender(Callback * apCallback, CommandHandlerImpl::Callback * apDispatchCallback) : mpCallback(apCallback), mpCommandHandlerCallback(apDispatchCallback), mCommandHandler(this), mExchangeCtx(*this) {} @@ -59,9 +59,9 @@ class CommandResponseSender : public Messaging::ExchangeDelegate, void OnResponseTimeout(Messaging::ExchangeContext * ec) override; - void OnDone(CommandHandler & apCommandObj) override; + void OnDone(CommandHandlerImpl & apCommandObj) override; - void DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, + void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) override; Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) override; @@ -149,7 +149,7 @@ class CommandResponseSender : public Messaging::ExchangeDelegate, * @param faultType The specific type of fault to inject into the response. */ void TestOnlyInvokeCommandRequestWithFaultsInjected(Messaging::ExchangeContext * ec, System::PacketBufferHandle && payload, - bool isTimedInvoke, CommandHandler::NlFaultInjectionType faultType); + bool isTimedInvoke, CommandHandlerImpl::NlFaultInjectionType faultType); #endif // CHIP_WITH_NLFAULTINJECTION private: @@ -182,8 +182,8 @@ class CommandResponseSender : public Messaging::ExchangeDelegate, System::PacketBufferHandle mChunks; Callback * mpCallback; - CommandHandler::Callback * mpCommandHandlerCallback; - CommandHandler mCommandHandler; + CommandHandlerImpl::Callback * mpCommandHandlerCallback; + CommandHandlerImpl mCommandHandler; Messaging::ExchangeHolder mExchangeCtx; State mState = State::ReadyForInvokeResponses; diff --git a/src/app/InteractionModelEngine.cpp b/src/app/InteractionModelEngine.cpp index 6ee2a1134594b5..132e8cdf108a72 100644 --- a/src/app/InteractionModelEngine.cpp +++ b/src/app/InteractionModelEngine.cpp @@ -389,8 +389,8 @@ void InteractionModelEngine::OnDone(CommandResponseSender & apResponderObj) mCommandResponderObjs.ReleaseObject(&apResponderObj); } -// TODO(#30453): Follow up refactor. Remove need for InteractionModelEngine::OnDone(CommandHandler). -void InteractionModelEngine::OnDone(CommandHandler & apCommandObj) +// TODO(#30453): Follow up refactor. Remove need for InteractionModelEngine::OnDone(CommandHandlerImpl). +void InteractionModelEngine::OnDone(CommandHandlerImpl & apCommandObj) { // We are no longer expecting to receive this callback. With the introduction of CommandResponseSender, it is now // responsible for receiving this callback. @@ -437,20 +437,20 @@ Status InteractionModelEngine::OnInvokeCommandRequest(Messaging::ExchangeContext ChipLogProgress(InteractionModel, "no resource for Invoke interaction"); return Status::Busy; } - CHIP_FAULT_INJECT( - FaultInjection::kFault_IMInvoke_SeparateResponses, - commandResponder->TestOnlyInvokeCommandRequestWithFaultsInjected( - apExchangeContext, std::move(aPayload), aIsTimedInvoke, CommandHandler::NlFaultInjectionType::SeparateResponseMessages); - return Status::Success;); + CHIP_FAULT_INJECT(FaultInjection::kFault_IMInvoke_SeparateResponses, + commandResponder->TestOnlyInvokeCommandRequestWithFaultsInjected( + apExchangeContext, std::move(aPayload), aIsTimedInvoke, + CommandHandlerImpl::NlFaultInjectionType::SeparateResponseMessages); + return Status::Success;); CHIP_FAULT_INJECT(FaultInjection::kFault_IMInvoke_SeparateResponsesInvertResponseOrder, commandResponder->TestOnlyInvokeCommandRequestWithFaultsInjected( apExchangeContext, std::move(aPayload), aIsTimedInvoke, - CommandHandler::NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder); + CommandHandlerImpl::NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder); return Status::Success;); CHIP_FAULT_INJECT( FaultInjection::kFault_IMInvoke_SkipSecondResponse, - commandResponder->TestOnlyInvokeCommandRequestWithFaultsInjected(apExchangeContext, std::move(aPayload), aIsTimedInvoke, - CommandHandler::NlFaultInjectionType::SkipSecondResponse); + commandResponder->TestOnlyInvokeCommandRequestWithFaultsInjected( + apExchangeContext, std::move(aPayload), aIsTimedInvoke, CommandHandlerImpl::NlFaultInjectionType::SkipSecondResponse); return Status::Success;); commandResponder->OnInvokeCommandRequest(apExchangeContext, std::move(aPayload), aIsTimedInvoke); return Status::Success; @@ -1674,7 +1674,7 @@ CHIP_ERROR InteractionModelEngine::PushFront(SingleLinkedListNode *& aObjectL return CHIP_NO_ERROR; } -void InteractionModelEngine::DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, +void InteractionModelEngine::DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) { CommandHandlerInterface * handler = FindCommandHandler(aCommandPath.mEndpointId, aCommandPath.mClusterId); @@ -1901,7 +1901,7 @@ void InteractionModelEngine::OnFabricRemoved(const FabricTable & fabricTable, Fa } } - // Applications may hold references to CommandHandler instances for async command processing. + // Applications may hold references to CommandHandlerImpl instances for async command processing. // Therefore we can't forcible destroy CommandHandlers here. Their exchanges will get closed by // the fabric removal, though, so they will fail when they try to actually send their command response // and will close at that point. diff --git a/src/app/InteractionModelEngine.h b/src/app/InteractionModelEngine.h index 4f61cb4e464a27..69c565254f96f0 100644 --- a/src/app/InteractionModelEngine.h +++ b/src/app/InteractionModelEngine.h @@ -31,7 +31,7 @@ #include #include #include -#include +#include #include #include #include @@ -87,7 +87,7 @@ namespace app { class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler, public Messaging::ExchangeDelegate, public CommandResponseSender::Callback, - public CommandHandler::Callback, + public CommandHandlerImpl::Callback, public ReadHandler::ManagementCallback, public FabricTable::Delegate, public SubscriptionsInfoProvider, @@ -419,7 +419,7 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler, using Status = Protocols::InteractionModel::Status; void OnDone(CommandResponseSender & apResponderObj) override; - void OnDone(CommandHandler & apCommandObj) override; + void OnDone(CommandHandlerImpl & apCommandObj) override; void OnDone(ReadHandler & apReadObj) override; void TryToResumeSubscriptions(); @@ -499,7 +499,7 @@ class InteractionModelEngine : public Messaging::UnsolicitedMessageHandler, Status OnUnsolicitedReportData(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload); - void DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, + void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) override; Protocols::InteractionModel::Status CommandExists(const ConcreteCommandPath & aCommandPath) override; diff --git a/src/app/tests/TestCommandInteraction.cpp b/src/app/tests/TestCommandInteraction.cpp index dc6b6132341a63..2a5791ba839423 100644 --- a/src/app/tests/TestCommandInteraction.cpp +++ b/src/app/tests/TestCommandInteraction.cpp @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -310,11 +311,12 @@ class MockCommandResponder : public CommandHandlerExchangeInterface bool mResponseDropped = false; }; -class MockCommandHandlerCallback : public CommandHandler::Callback +class MockCommandHandlerCallback : public CommandHandlerImpl::Callback { public: - void OnDone(CommandHandler & apCommandHandler) final { onFinalCalledTimes++; } - void DispatchCommand(CommandHandler & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) final + void OnDone(CommandHandlerImpl & apCommandHandler) final { onFinalCalledTimes++; } + void DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, + TLV::TLVReader & apPayload) final { DispatchSingleClusterCommand(aCommandPath, apPayload, &apCommandObj); } @@ -395,17 +397,19 @@ class TestCommandInteraction * we want to test APIs that cluster code uses, we need to inject entries into the * CommandPathRegistry directly. */ - class CommandHandlerWithUnrespondedCommand : public app::CommandHandler + class CommandHandlerWithUnrespondedCommand : public app::CommandHandlerImpl { public: - CommandHandlerWithUnrespondedCommand(CommandHandler::Callback * apCallback, const ConcreteCommandPath & aRequestCommandPath, - const Optional & aRef) : - CommandHandler(apCallback) + CommandHandlerWithUnrespondedCommand(CommandHandlerImpl::Callback * apCallback, + const ConcreteCommandPath & aRequestCommandPath, const Optional & aRef) : + CommandHandlerImpl(apCallback) { GetCommandPathRegistry().Add(aRequestCommandPath, aRef.std_optional()); SetExchangeInterface(&mMockCommandResponder); } MockCommandResponder mMockCommandResponder; + + using app::CommandHandler::AddResponse; }; // Generate an invoke request. If aCommandId is kTestCommandIdWithData, a @@ -428,7 +432,7 @@ class TestCommandInteraction CommandId aRequestCommandId = kTestCommandIdWithData); static uint32_t GetAddResponseDataOverheadSizeForPath(nlTestSuite * apSuite, const ConcreteCommandPath & aRequestCommandPath, ForcedSizeBufferLengthHint aBufferSizeHint); - static void FillCurrentInvokeResponseBuffer(nlTestSuite * apSuite, CommandHandler * apCommandHandler, + static void FillCurrentInvokeResponseBuffer(nlTestSuite * apSuite, CommandHandlerImpl * apCommandHandler, const ConcreteCommandPath & aRequestCommandPath, uint32_t aSizeToLeaveInBuffer); static void ValidateCommandHandlerEncodeInvokeResponseMessage(nlTestSuite * apSuite, void * apContext, bool aNeedStatusCode); }; @@ -627,8 +631,8 @@ uint32_t TestCommandInteraction::GetAddResponseDataOverheadSizeForPath(nlTestSui { BasicCommandPathRegistry<4> basicCommandPathRegistry; MockCommandResponder mockCommandResponder; - CommandHandler::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; - CommandHandler commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); + CommandHandlerImpl::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; + CommandHandlerImpl commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); commandHandler.mReserveSpaceForMoreChunkMessages = true; ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage }; ConcreteCommandPath requestCommandPath2 = { kTestEndpointId, kTestClusterId, kTestCommandIdCommandSpecificResponse }; @@ -653,7 +657,7 @@ uint32_t TestCommandInteraction::GetAddResponseDataOverheadSizeForPath(nlTestSui return delta; } -void TestCommandInteraction::FillCurrentInvokeResponseBuffer(nlTestSuite * apSuite, CommandHandler * apCommandHandler, +void TestCommandInteraction::FillCurrentInvokeResponseBuffer(nlTestSuite * apSuite, CommandHandlerImpl * apCommandHandler, const ConcreteCommandPath & aRequestCommandPath, uint32_t aSizeToLeaveInBuffer) { @@ -705,7 +709,7 @@ void TestCommandInteraction::TestCommandHandlerWithWrongState(nlTestSuite * apSu // be handle already acquired on the callers behalf. CommandHandler::Handle handle(&commandHandler); - const CommandHandler::InvokeResponseParameters prepareParams(requestCommandPath); + const CommandHandlerImpl::InvokeResponseParameters prepareParams(requestCommandPath); err = commandHandler.PrepareInvokeResponseCommand(responseCommandPath, prepareParams); NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); } @@ -748,7 +752,7 @@ void TestCommandInteraction::TestCommandHandlerWithSendEmptyCommand(nlTestSuite // be handle already acquired on the callers behalf. CommandHandler::Handle handle(&commandHandler); - const CommandHandler::InvokeResponseParameters prepareParams(requestCommandPath); + const CommandHandlerImpl::InvokeResponseParameters prepareParams(requestCommandPath); err = commandHandler.PrepareInvokeResponseCommand(responseCommandPath, prepareParams); NL_TEST_ASSERT(apSuite, err == CHIP_NO_ERROR); err = commandHandler.FinishCommand(); @@ -892,8 +896,8 @@ struct BadFields void TestCommandInteraction::TestCommandHandlerCommandDataEncoding(nlTestSuite * apSuite, void * apContext) { - auto path = MakeTestCommandPath(); - auto requestCommandPath = ConcreteCommandPath(path.mEndpointId, path.mClusterId, path.mCommandId); + auto path = MakeTestCommandPath(); + ConcreteCommandPath requestCommandPath(path.mEndpointId, path.mClusterId, path.mCommandId); CommandHandlerWithUnrespondedCommand commandHandler(nullptr, requestCommandPath, /* aRef = */ NullOptional); { @@ -1295,7 +1299,7 @@ void TestCommandInteraction::TestCommandHandlerEncodeSimpleStatusCode(nlTestSuit void TestCommandInteraction::TestCommandHandlerWithoutResponderCallingAddStatus(nlTestSuite * apSuite, void * apContext) { chip::app::ConcreteCommandPath requestCommandPath(kTestEndpointId, kTestClusterId, kTestCommandIdWithData); - CommandHandler commandHandler(&mockCommandHandlerDelegate); + CommandHandlerImpl commandHandler(&mockCommandHandlerDelegate); commandHandler.AddStatus(requestCommandPath, Protocols::InteractionModel::Status::Failure); @@ -1307,7 +1311,7 @@ void TestCommandInteraction::TestCommandHandlerWithoutResponderCallingAddStatus( void TestCommandInteraction::TestCommandHandlerWithoutResponderCallingAddResponse(nlTestSuite * apSuite, void * apContext) { chip::app::ConcreteCommandPath requestCommandPath(kTestEndpointId, kTestClusterId, kTestCommandIdWithData); - CommandHandler commandHandler(&mockCommandHandlerDelegate); + CommandHandlerImpl commandHandler(&mockCommandHandlerDelegate); uint32_t sizeToFill = 50; // This is an arbitrary number, we need to select a non-zero value. CHIP_ERROR err = commandHandler.AddResponseData(requestCommandPath, ForcedSizeBuffer(sizeToFill)); @@ -1322,13 +1326,13 @@ void TestCommandInteraction::TestCommandHandlerWithoutResponderCallingDirectPrep void * apContext) { chip::app::ConcreteCommandPath requestCommandPath(kTestEndpointId, kTestClusterId, kTestCommandIdWithData); - CommandHandler commandHandler(&mockCommandHandlerDelegate); + CommandHandlerImpl commandHandler(&mockCommandHandlerDelegate); // We intentionally prevent successful calls to PrepareInvokeResponseCommand and FinishCommand when no // responder is present. This aligns with the design decision to promote AddStatus and AddResponseData // usage in such scenarios. See GitHub issue #32486 for discussions on phasing out external use of // these primitives. - const CommandHandler::InvokeResponseParameters prepareParams(requestCommandPath); + const CommandHandlerImpl::InvokeResponseParameters prepareParams(requestCommandPath); ConcreteCommandPath responseCommandPath = { kTestEndpointId, kTestClusterId, kTestCommandIdCommandSpecificResponse }; CHIP_ERROR err = commandHandler.PrepareInvokeResponseCommand(responseCommandPath, prepareParams); NL_TEST_ASSERT(apSuite, err == CHIP_ERROR_INCORRECT_STATE); @@ -1347,7 +1351,7 @@ void TestCommandInteraction::TestCommandHandlerWithOnInvokeReceivedNotExistComma // Use some invalid endpoint / cluster / command. GenerateInvokeRequest(apSuite, apContext, commandDatabuf, /* aIsTimedRequest = */ false, 0xEF /* command */, 0xADBE /* cluster */, 0xDE /* endpoint */); - CommandHandler commandHandler(&mockCommandHandlerDelegate); + CommandHandlerImpl commandHandler(&mockCommandHandlerDelegate); chip::isCommandDispatched = false; mockCommandHandlerDelegate.ResetCounter(); @@ -1368,7 +1372,7 @@ void TestCommandInteraction::TestCommandHandlerWithOnInvokeReceivedEmptyDataMsg( for (auto transactionIsTimed : allBooleans) { mockCommandHandlerDelegate.ResetCounter(); - CommandHandler commandHandler(&mockCommandHandlerDelegate); + CommandHandlerImpl commandHandler(&mockCommandHandlerDelegate); System::PacketBufferHandle commandDatabuf = System::PacketBufferHandle::New(System::PacketBuffer::kMaxSize); chip::isCommandDispatched = false; @@ -1800,8 +1804,8 @@ void TestCommandInteraction::TestCommandHandlerRejectsMultipleCommandsWithIdenti BasicCommandPathRegistry<4> basicCommandPathRegistry; MockCommandResponder mockCommandResponder; - CommandHandler::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; - CommandHandler commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); + CommandHandlerImpl::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; + CommandHandlerImpl commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); // Hackery to steal the InvokeRequest buffer from commandSender. System::PacketBufferHandle commandDatabuf; @@ -1867,7 +1871,7 @@ void TestCommandInteraction::TestCommandHandlerRejectMultipleCommandsWhenHandler sendResponse = true; commandDispatchedCount = 0; - CommandHandler commandHandler(&mockCommandHandlerDelegate); + CommandHandlerImpl commandHandler(&mockCommandHandlerDelegate); MockCommandResponder mockCommandResponder; InteractionModel::Status status = commandHandler.OnInvokeCommandRequest(mockCommandResponder, std::move(commandDatabuf), false); NL_TEST_ASSERT(apSuite, status == InteractionModel::Status::InvalidAction); @@ -1918,8 +1922,8 @@ void TestCommandInteraction::TestCommandHandlerAcceptMultipleCommands(nlTestSuit BasicCommandPathRegistry<4> basicCommandPathRegistry; MockCommandResponder mockCommandResponder; - CommandHandler::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; - CommandHandler commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); + CommandHandlerImpl::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; + CommandHandlerImpl commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); // Hackery to steal the InvokeRequest buffer from commandSender. System::PacketBufferHandle commandDatabuf; @@ -1941,8 +1945,8 @@ void TestCommandInteraction::TestCommandHandler_FillUpInvokeResponseMessageWhere { BasicCommandPathRegistry<4> basicCommandPathRegistry; MockCommandResponder mockCommandResponder; - CommandHandler::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; - CommandHandler commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); + CommandHandlerImpl::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; + CommandHandlerImpl commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); commandHandler.mReserveSpaceForMoreChunkMessages = true; ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage }; @@ -1970,8 +1974,8 @@ void TestCommandInteraction::TestCommandHandler_FillUpInvokeResponseMessageWhere { BasicCommandPathRegistry<4> basicCommandPathRegistry; MockCommandResponder mockCommandResponder; - CommandHandler::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; - CommandHandler commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); + CommandHandlerImpl::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; + CommandHandlerImpl commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); commandHandler.mReserveSpaceForMoreChunkMessages = true; ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage }; @@ -1999,8 +2003,8 @@ void TestCommandInteraction::TestCommandHandler_FillUpInvokeResponseMessageWhere { BasicCommandPathRegistry<4> basicCommandPathRegistry; MockCommandResponder mockCommandResponder; - CommandHandler::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; - CommandHandler commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); + CommandHandlerImpl::TestOnlyOverrides testOnlyOverrides{ &basicCommandPathRegistry, &mockCommandResponder }; + CommandHandlerImpl commandHandler(testOnlyOverrides, &mockCommandHandlerDelegate); commandHandler.mReserveSpaceForMoreChunkMessages = true; ConcreteCommandPath requestCommandPath1 = { kTestEndpointId, kTestClusterId, kTestCommandIdFillResponseMessage }; ConcreteCommandPath requestCommandPath2 = { kTestEndpointId, kTestClusterId, kTestCommandIdCommandSpecificResponse }; @@ -2049,9 +2053,8 @@ void TestCommandInteraction::TestCommandHandlerReleaseWithExchangeClosed(nlTestS // Mimic closure of the exchange that would happen on a session release and verify that releasing the handle there-after // is handled gracefully. - asyncCommandHandle.Get()->mpResponder->GetExchangeContext()->GetSessionHolder().Release(); - asyncCommandHandle.Get()->mpResponder->GetExchangeContext()->OnSessionReleased(); - + asyncCommandHandle.Get()->GetExchangeContext()->GetSessionHolder().Release(); + asyncCommandHandle.Get()->GetExchangeContext()->OnSessionReleased(); asyncCommandHandle = nullptr; } #endif diff --git a/src/controller/java/OTAProviderDelegateBridge.cpp b/src/controller/java/OTAProviderDelegateBridge.cpp index 4565d42416c19a..77bf3572e0f65c 100644 --- a/src/controller/java/OTAProviderDelegateBridge.cpp +++ b/src/controller/java/OTAProviderDelegateBridge.cpp @@ -18,6 +18,7 @@ #include "OTAProviderDelegateBridge.h" +#include #include #include #include