/* * * Copyright (c) 2020-2021 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. */ /** * @file * This file defines objects for a CHIP Interaction Data model Engine which handle unsolicited IM message, and * manage different kinds of IM client and handlers. * */ #include "InteractionModelEngine.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE // TODO: defaulting to codegen should eventually be an application choice and not // hard-coded in the interaction model #include #endif #if !CHIP_CONFIG_USE_DATA_MODEL_INTERFACE #include // nogncheck #endif namespace chip { namespace app { class AutoReleaseSubscriptionInfoIterator { public: AutoReleaseSubscriptionInfoIterator(SubscriptionResumptionStorage::SubscriptionInfoIterator * iterator) : mIterator(iterator){}; ~AutoReleaseSubscriptionInfoIterator() { mIterator->Release(); } SubscriptionResumptionStorage::SubscriptionInfoIterator * operator->() const { return mIterator; } private: SubscriptionResumptionStorage::SubscriptionInfoIterator * mIterator; }; using Protocols::InteractionModel::Status; Global sInteractionModelEngine; InteractionModelEngine::InteractionModelEngine() : mReportingEngine(this) {} InteractionModelEngine * InteractionModelEngine::GetInstance() { return &sInteractionModelEngine.get(); } CHIP_ERROR InteractionModelEngine::Init(Messaging::ExchangeManager * apExchangeMgr, FabricTable * apFabricTable, reporting::ReportScheduler * reportScheduler, CASESessionManager * apCASESessionMgr, SubscriptionResumptionStorage * subscriptionResumptionStorage) { VerifyOrReturnError(apFabricTable != nullptr, CHIP_ERROR_INVALID_ARGUMENT); VerifyOrReturnError(apExchangeMgr != nullptr, CHIP_ERROR_INVALID_ARGUMENT); VerifyOrReturnError(reportScheduler != nullptr, CHIP_ERROR_INVALID_ARGUMENT); mState = State::kInitializing; mpExchangeMgr = apExchangeMgr; mpFabricTable = apFabricTable; mpCASESessionMgr = apCASESessionMgr; mpSubscriptionResumptionStorage = subscriptionResumptionStorage; mReportScheduler = reportScheduler; ReturnErrorOnFailure(mpFabricTable->AddFabricDelegate(this)); ReturnErrorOnFailure(mpExchangeMgr->RegisterUnsolicitedMessageHandlerForProtocol(Protocols::InteractionModel::Id, this)); mReportingEngine.Init(); StatusIB::RegisterErrorFormatter(); #if CHIP_CONFIG_USE_EMBER_DATA_MODEL && CHIP_CONFIG_USE_DATA_MODEL_INTERFACE ChipLogError(InteractionModel, "WARNING ┌────────────────────────────────────────────────────"); ChipLogError(InteractionModel, "WARNING │ Interaction Model Engine running in 'Checked' mode."); ChipLogError(InteractionModel, "WARNING │ This executes BOTH ember and data-model code paths."); ChipLogError(InteractionModel, "WARNING │ which is inefficient and consumes more flash space."); ChipLogError(InteractionModel, "WARNING │ This should be done for testing only."); ChipLogError(InteractionModel, "WARNING └────────────────────────────────────────────────────"); #endif mState = State::kInitialized; return CHIP_NO_ERROR; } void InteractionModelEngine::Shutdown() { VerifyOrReturn(State::kUninitialized != mState); mpExchangeMgr->GetSessionManager()->SystemLayer()->CancelTimer(ResumeSubscriptionsTimerCallback, this); // TODO: individual object clears the entire command handler interface registry. // This may not be expected as IME does NOT own the command handler interface registry. // // This is to be cleaned up once InteractionModelEngine maintains a data model fully and // the code-generation model can do its clear in its shutdown method. CommandHandlerInterfaceRegistry::Instance().UnregisterAllHandlers(); mCommandResponderObjs.ReleaseAll(); mTimedHandlers.ForEachActiveObject([this](TimedHandler * obj) -> Loop { mpExchangeMgr->CloseAllContextsForDelegate(obj); return Loop::Continue; }); mTimedHandlers.ReleaseAll(); mReadHandlers.ReleaseAll(); #if CHIP_CONFIG_ENABLE_READ_CLIENT // Shut down any subscription clients that are still around. They won't be // able to work after this point anyway, since we're about to drop our refs // to them. ShutdownAllSubscriptions(); // // We hold weak references to ReadClient objects. The application ultimately // actually owns them, so it's on them to eventually shut them down and free them // up. // // However, we should null out their pointers back to us at the very least so that // at destruction time, they won't attempt to reach back here to remove themselves // from this list. // for (auto * readClient = mpActiveReadClientList; readClient != nullptr;) { readClient->mpImEngine = nullptr; auto * tmpClient = readClient->GetNextClient(); readClient->SetNextClient(nullptr); readClient = tmpClient; } // // After that, we just null out our tracker. // mpActiveReadClientList = nullptr; #endif // CHIP_CONFIG_ENABLE_READ_CLIENT for (auto & writeHandler : mWriteHandlers) { if (!writeHandler.IsFree()) { writeHandler.Close(); } } mReportingEngine.Shutdown(); mAttributePathPool.ReleaseAll(); mEventPathPool.ReleaseAll(); mDataVersionFilterPool.ReleaseAll(); mpExchangeMgr->UnregisterUnsolicitedMessageHandlerForProtocol(Protocols::InteractionModel::Id); mpCASESessionMgr = nullptr; // // We _should_ be clearing these out, but doing so invites a world // of trouble. #21233 tracks fixing the underlying assumptions to make // this possible. // // mpFabricTable = nullptr; // mpExchangeMgr = nullptr; mState = State::kUninitialized; } uint32_t InteractionModelEngine::GetNumActiveReadHandlers() const { return static_cast(mReadHandlers.Allocated()); } uint32_t InteractionModelEngine::GetNumActiveReadHandlers(ReadHandler::InteractionType aType) const { uint32_t count = 0; mReadHandlers.ForEachActiveObject([aType, &count](const ReadHandler * handler) { if (handler->IsType(aType)) { count++; } return Loop::Continue; }); return count; } uint32_t InteractionModelEngine::GetNumActiveReadHandlers(ReadHandler::InteractionType aType, FabricIndex aFabricIndex) const { uint32_t count = 0; mReadHandlers.ForEachActiveObject([aType, aFabricIndex, &count](const ReadHandler * handler) { if (handler->IsType(aType) && handler->GetAccessingFabricIndex() == aFabricIndex) { count++; } return Loop::Continue; }); return count; } ReadHandler * InteractionModelEngine::ActiveHandlerAt(unsigned int aIndex) { if (aIndex >= mReadHandlers.Allocated()) { return nullptr; } unsigned int i = 0; ReadHandler * ret = nullptr; mReadHandlers.ForEachActiveObject([aIndex, &i, &ret](ReadHandler * handler) { if (i == aIndex) { ret = handler; return Loop::Break; } i++; return Loop::Continue; }); return ret; } WriteHandler * InteractionModelEngine::ActiveWriteHandlerAt(unsigned int aIndex) { unsigned int i = 0; for (auto & writeHandler : mWriteHandlers) { if (!writeHandler.IsFree()) { if (i == aIndex) { return &writeHandler; } i++; } } return nullptr; } uint32_t InteractionModelEngine::GetNumActiveWriteHandlers() const { uint32_t numActive = 0; for (auto & writeHandler : mWriteHandlers) { if (!writeHandler.IsFree()) { numActive++; } } return numActive; } #if CHIP_CONFIG_ENABLE_READ_CLIENT CHIP_ERROR InteractionModelEngine::ShutdownSubscription(const ScopedNodeId & aPeerNodeId, SubscriptionId aSubscriptionId) { assertChipStackLockedByCurrentThread(); for (auto * readClient = mpActiveReadClientList; readClient != nullptr;) { // Grab the next client now, because we might be about to delete readClient. auto * nextClient = readClient->GetNextClient(); if (readClient->IsSubscriptionType() && readClient->IsMatchingSubscriptionId(aSubscriptionId) && readClient->GetFabricIndex() == aPeerNodeId.GetFabricIndex() && readClient->GetPeerNodeId() == aPeerNodeId.GetNodeId()) { readClient->Close(CHIP_NO_ERROR); return CHIP_NO_ERROR; } readClient = nextClient; } return CHIP_ERROR_KEY_NOT_FOUND; } void InteractionModelEngine::ShutdownSubscriptions(FabricIndex aFabricIndex, NodeId aPeerNodeId) { assertChipStackLockedByCurrentThread(); ShutdownMatchingSubscriptions(MakeOptional(aFabricIndex), MakeOptional(aPeerNodeId)); } void InteractionModelEngine::ShutdownSubscriptions(FabricIndex aFabricIndex) { assertChipStackLockedByCurrentThread(); ShutdownMatchingSubscriptions(MakeOptional(aFabricIndex)); } void InteractionModelEngine::ShutdownAllSubscriptions() { assertChipStackLockedByCurrentThread(); ShutdownMatchingSubscriptions(); } void InteractionModelEngine::ShutdownMatchingSubscriptions(const Optional & aFabricIndex, const Optional & aPeerNodeId) { // This is assuming that ReadClient::Close will not affect any other // ReadClients in the list. for (auto * readClient = mpActiveReadClientList; readClient != nullptr;) { // Grab the next client now, because we might be about to delete readClient. auto * nextClient = readClient->GetNextClient(); if (readClient->IsSubscriptionType()) { bool fabricMatches = !aFabricIndex.HasValue() || (aFabricIndex.Value() == readClient->GetFabricIndex()); bool nodeIdMatches = !aPeerNodeId.HasValue() || (aPeerNodeId.Value() == readClient->GetPeerNodeId()); if (fabricMatches && nodeIdMatches) { readClient->Close(CHIP_NO_ERROR); } } readClient = nextClient; } } #endif // CHIP_CONFIG_ENABLE_READ_CLIENT bool InteractionModelEngine::SubjectHasActiveSubscription(FabricIndex aFabricIndex, NodeId subjectID) { bool isActive = false; mReadHandlers.ForEachActiveObject([aFabricIndex, subjectID, &isActive](ReadHandler * handler) { VerifyOrReturnValue(handler->IsType(ReadHandler::InteractionType::Subscribe), Loop::Continue); Access::SubjectDescriptor subject = handler->GetSubjectDescriptor(); VerifyOrReturnValue(subject.fabricIndex == aFabricIndex, Loop::Continue); if (subject.authMode == Access::AuthMode::kCase) { if (subject.cats.CheckSubjectAgainstCATs(subjectID) || subjectID == subject.subject) { isActive = handler->IsActiveSubscription(); // Exit loop only if isActive is set to true. // Otherwise keep looking for another subscription that could match the subject. VerifyOrReturnValue(!isActive, Loop::Break); } } return Loop::Continue; }); return isActive; } bool InteractionModelEngine::SubjectHasPersistedSubscription(FabricIndex aFabricIndex, NodeId subjectID) { bool persistedSubMatches = false; #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS auto * iterator = mpSubscriptionResumptionStorage->IterateSubscriptions(); // Verify that we were able to allocate an iterator. If not, we are probably currently trying to resubscribe to our persisted // subscriptions. As such, we assume we have a persisted subscription and return true. // If we don't have a persisted subscription for the given fabric index and subjectID, we will send a Check-In message next time // we transition to ActiveMode. VerifyOrReturnValue(iterator, true); SubscriptionResumptionStorage::SubscriptionInfo subscriptionInfo; while (iterator->Next(subscriptionInfo)) { // TODO(#31873): Persistent subscription only stores the NodeID for now. We cannot check if the CAT matches if (subscriptionInfo.mFabricIndex == aFabricIndex && subscriptionInfo.mNodeId == subjectID) { persistedSubMatches = true; break; } } iterator->Release(); #endif // CHIP_CONFIG_PERSIST_SUBSCRIPTIONS return persistedSubMatches; } void InteractionModelEngine::OnDone(CommandResponseSender & apResponderObj) { mCommandResponderObjs.ReleaseObject(&apResponderObj); } // 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. VerifyOrDie(false); } void InteractionModelEngine::OnDone(ReadHandler & apReadObj) { // // Deleting an item can shift down the contents of the underlying pool storage, // rendering any tracker using positional indexes invalid. Let's reset it, // based on which readHandler we are getting rid of. // mReportingEngine.ResetReadHandlerTracker(&apReadObj); mReadHandlers.ReleaseObject(&apReadObj); TryToResumeSubscriptions(); } void InteractionModelEngine::TryToResumeSubscriptions() { #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS && CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION if (!mSubscriptionResumptionScheduled && HasSubscriptionsToResume()) { mSubscriptionResumptionScheduled = true; auto timeTillNextSubscriptionResumptionSecs = ComputeTimeSecondsTillNextSubscriptionResumption(); mpExchangeMgr->GetSessionManager()->SystemLayer()->StartTimer( System::Clock::Seconds32(timeTillNextSubscriptionResumptionSecs), ResumeSubscriptionsTimerCallback, this); mNumSubscriptionResumptionRetries++; ChipLogProgress(InteractionModel, "Schedule subscription resumption when failing to establish session, Retries: %" PRIu32, mNumSubscriptionResumptionRetries); } #endif // CHIP_CONFIG_PERSIST_SUBSCRIPTIONS && CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION } Status InteractionModelEngine::OnInvokeCommandRequest(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload, bool aIsTimedInvoke) { // TODO(#30453): Refactor CommandResponseSender's constructor to accept an exchange context parameter. CommandResponseSender * commandResponder = mCommandResponderObjs.CreateObject(this, this); if (commandResponder == nullptr) { ChipLogProgress(InteractionModel, "no resource for Invoke interaction"); return Status::Busy; } 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, CommandHandlerImpl::NlFaultInjectionType::SeparateResponseMessagesAndInvertedResponseOrder); return Status::Success;); CHIP_FAULT_INJECT( FaultInjection::kFault_IMInvoke_SkipSecondResponse, commandResponder->TestOnlyInvokeCommandRequestWithFaultsInjected( apExchangeContext, std::move(aPayload), aIsTimedInvoke, CommandHandlerImpl::NlFaultInjectionType::SkipSecondResponse); return Status::Success;); commandResponder->OnInvokeCommandRequest(apExchangeContext, std::move(aPayload), aIsTimedInvoke); return Status::Success; } CHIP_ERROR InteractionModelEngine::ParseAttributePaths(const Access::SubjectDescriptor & aSubjectDescriptor, AttributePathIBs::Parser & aAttributePathListParser, bool & aHasValidAttributePath, size_t & aRequestedAttributePathCount) { TLV::TLVReader pathReader; aAttributePathListParser.GetReader(&pathReader); CHIP_ERROR err = CHIP_NO_ERROR; aHasValidAttributePath = false; aRequestedAttributePathCount = 0; while (CHIP_NO_ERROR == (err = pathReader.Next(TLV::AnonymousTag()))) { AttributePathIB::Parser path; // // We create an iterator to point to a single item object list that tracks the path we just parsed. // This avoids the 'parse all paths' approach that is employed in ReadHandler since we want to // avoid allocating out of the path store during this minimal initial processing stage. // SingleLinkedListNode paramsList; ReturnErrorOnFailure(path.Init(pathReader)); ReturnErrorOnFailure(path.ParsePath(paramsList.mValue)); if (paramsList.mValue.IsWildcardPath()) { AttributePathExpandIterator pathIterator(GetDataModelProvider(), ¶msList); ConcreteAttributePath readPath; // The definition of "valid path" is "path exists and ACL allows access". The "path exists" part is handled by // AttributePathExpandIterator. So we just need to check the ACL bits. for (; pathIterator.Get(readPath); pathIterator.Next()) { // leave requestPath.entityId optional value unset to indicate wildcard Access::RequestPath requestPath{ .cluster = readPath.mClusterId, .endpoint = readPath.mEndpointId, .requestType = Access::RequestType::kAttributeReadRequest }; err = Access::GetAccessControl().Check(aSubjectDescriptor, requestPath, RequiredPrivilege::ForReadAttribute(readPath)); if (err == CHIP_NO_ERROR) { aHasValidAttributePath = true; break; } } } else { ConcreteAttributePath concretePath(paramsList.mValue.mEndpointId, paramsList.mValue.mClusterId, paramsList.mValue.mAttributeId); if (IsExistentAttributePath(concretePath)) { Access::RequestPath requestPath{ .cluster = concretePath.mClusterId, .endpoint = concretePath.mEndpointId, .requestType = Access::RequestType::kAttributeReadRequest, .entityId = paramsList.mValue.mAttributeId }; err = Access::GetAccessControl().Check(aSubjectDescriptor, requestPath, RequiredPrivilege::ForReadAttribute(concretePath)); if (err == CHIP_NO_ERROR) { aHasValidAttributePath = true; } } } aRequestedAttributePathCount++; } if (err == CHIP_ERROR_END_OF_TLV) { err = CHIP_NO_ERROR; } return err; } CHIP_ERROR InteractionModelEngine::ParseEventPaths(const Access::SubjectDescriptor & aSubjectDescriptor, EventPathIBs::Parser & aEventPathListParser, bool & aHasValidEventPath, size_t & aRequestedEventPathCount) { TLV::TLVReader pathReader; aEventPathListParser.GetReader(&pathReader); CHIP_ERROR err = CHIP_NO_ERROR; aHasValidEventPath = false; aRequestedEventPathCount = 0; while (CHIP_NO_ERROR == (err = pathReader.Next(TLV::AnonymousTag()))) { EventPathIB::Parser path; ReturnErrorOnFailure(path.Init(pathReader)); EventPathParams eventPath; ReturnErrorOnFailure(path.ParsePath(eventPath)); ++aRequestedEventPathCount; if (aHasValidEventPath) { // Can skip all the rest of the checking. continue; } #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE aHasValidEventPath = mDataModelProvider->EventPathIncludesAccessibleConcretePath(eventPath, aSubjectDescriptor); #else // The definition of "valid path" is "path exists and ACL allows // access". We need to do some expansion of wildcards to handle that. if (eventPath.HasWildcardEndpointId()) { for (uint16_t endpointIndex = 0; !aHasValidEventPath && endpointIndex < emberAfEndpointCount(); ++endpointIndex) { if (!emberAfEndpointIndexIsEnabled(endpointIndex)) { continue; } aHasValidEventPath = HasValidEventPathForEndpoint(emberAfEndpointFromIndex(endpointIndex), eventPath, aSubjectDescriptor); } } else { // No need to check whether the endpoint is enabled, because // emberAfFindEndpointType returns null for disabled endpoints. aHasValidEventPath = HasValidEventPathForEndpoint(eventPath.mEndpointId, eventPath, aSubjectDescriptor); } #endif // CHIP_CONFIG_USE_EMBER_DATA_MODEL } if (err == CHIP_ERROR_END_OF_TLV) { err = CHIP_NO_ERROR; } return err; } Protocols::InteractionModel::Status InteractionModelEngine::OnReadInitialRequest(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload, ReadHandler::InteractionType aInteractionType) { ChipLogDetail(InteractionModel, "Received %s request", aInteractionType == ReadHandler::InteractionType::Subscribe ? "Subscribe" : "Read"); // // Let's first figure out if the client has sent us a subscribe request and requested we keep any existing // subscriptions from that source. // if (aInteractionType == ReadHandler::InteractionType::Subscribe) { System::PacketBufferTLVReader reader; bool keepExistingSubscriptions = true; if (apExchangeContext->GetSessionHandle()->GetFabricIndex() == kUndefinedFabricIndex) { // Subscriptions must be associated to a fabric. return Status::UnsupportedAccess; } reader.Init(aPayload.Retain()); SubscribeRequestMessage::Parser subscribeRequestParser; VerifyOrReturnError(subscribeRequestParser.Init(reader) == CHIP_NO_ERROR, Status::InvalidAction); #if CHIP_CONFIG_IM_PRETTY_PRINT subscribeRequestParser.PrettyPrint(); #endif VerifyOrReturnError(subscribeRequestParser.GetKeepSubscriptions(&keepExistingSubscriptions) == CHIP_NO_ERROR, Status::InvalidAction); if (!keepExistingSubscriptions) { // // Walk through all existing subscriptions and shut down those whose subscriber matches // that which just came in. // mReadHandlers.ForEachActiveObject([apExchangeContext](ReadHandler * handler) { if (handler->IsFromSubscriber(*apExchangeContext)) { ChipLogProgress(InteractionModel, "Deleting previous subscription from NodeId: " ChipLogFormatX64 ", FabricIndex: %u", ChipLogValueX64(apExchangeContext->GetSessionHandle()->AsSecureSession()->GetPeerNodeId()), apExchangeContext->GetSessionHandle()->GetFabricIndex()); handler->Close(); } return Loop::Continue; }); } { size_t requestedAttributePathCount = 0; size_t requestedEventPathCount = 0; AttributePathIBs::Parser attributePathListParser; bool hasValidAttributePath = false; bool hasValidEventPath = false; CHIP_ERROR err = subscribeRequestParser.GetAttributeRequests(&attributePathListParser); if (err == CHIP_NO_ERROR) { auto subjectDescriptor = apExchangeContext->GetSessionHandle()->AsSecureSession()->GetSubjectDescriptor(); err = ParseAttributePaths(subjectDescriptor, attributePathListParser, hasValidAttributePath, requestedAttributePathCount); if (err != CHIP_NO_ERROR) { return Status::InvalidAction; } } else if (err != CHIP_ERROR_END_OF_TLV) { return Status::InvalidAction; } EventPathIBs::Parser eventPathListParser; err = subscribeRequestParser.GetEventRequests(&eventPathListParser); if (err == CHIP_NO_ERROR) { auto subjectDescriptor = apExchangeContext->GetSessionHandle()->AsSecureSession()->GetSubjectDescriptor(); err = ParseEventPaths(subjectDescriptor, eventPathListParser, hasValidEventPath, requestedEventPathCount); if (err != CHIP_NO_ERROR) { return Status::InvalidAction; } } else if (err != CHIP_ERROR_END_OF_TLV) { return Status::InvalidAction; } if (requestedAttributePathCount == 0 && requestedEventPathCount == 0) { ChipLogError(InteractionModel, "Subscription from [%u:" ChipLogFormatX64 "] has no attribute or event paths. Rejecting request.", apExchangeContext->GetSessionHandle()->GetFabricIndex(), ChipLogValueX64(apExchangeContext->GetSessionHandle()->AsSecureSession()->GetPeerNodeId())); return Status::InvalidAction; } if (!hasValidAttributePath && !hasValidEventPath) { ChipLogError(InteractionModel, "Subscription from [%u:" ChipLogFormatX64 "] has no access at all. Rejecting request.", apExchangeContext->GetSessionHandle()->GetFabricIndex(), ChipLogValueX64(apExchangeContext->GetSessionHandle()->AsSecureSession()->GetPeerNodeId())); return Status::InvalidAction; } // The following cast is safe, since we can only hold a few tens of paths in one request. if (!EnsureResourceForSubscription(apExchangeContext->GetSessionHandle()->GetFabricIndex(), requestedAttributePathCount, requestedEventPathCount)) { return Status::PathsExhausted; } } } else { System::PacketBufferTLVReader reader; reader.Init(aPayload.Retain()); ReadRequestMessage::Parser readRequestParser; VerifyOrReturnError(readRequestParser.Init(reader) == CHIP_NO_ERROR, Status::InvalidAction); #if CHIP_CONFIG_IM_PRETTY_PRINT readRequestParser.PrettyPrint(); #endif { size_t requestedAttributePathCount = 0; size_t requestedEventPathCount = 0; AttributePathIBs::Parser attributePathListParser; CHIP_ERROR err = readRequestParser.GetAttributeRequests(&attributePathListParser); if (err == CHIP_NO_ERROR) { TLV::TLVReader pathReader; attributePathListParser.GetReader(&pathReader); VerifyOrReturnError(TLV::Utilities::Count(pathReader, requestedAttributePathCount, false) == CHIP_NO_ERROR, Status::InvalidAction); } else if (err != CHIP_ERROR_END_OF_TLV) { return Status::InvalidAction; } EventPathIBs::Parser eventpathListParser; err = readRequestParser.GetEventRequests(&eventpathListParser); if (err == CHIP_NO_ERROR) { TLV::TLVReader pathReader; eventpathListParser.GetReader(&pathReader); VerifyOrReturnError(TLV::Utilities::Count(pathReader, requestedEventPathCount, false) == CHIP_NO_ERROR, Status::InvalidAction); } else if (err != CHIP_ERROR_END_OF_TLV) { return Status::InvalidAction; } // The following cast is safe, since we can only hold a few tens of paths in one request. Status checkResult = EnsureResourceForRead(apExchangeContext->GetSessionHandle()->GetFabricIndex(), requestedAttributePathCount, requestedEventPathCount); if (checkResult != Status::Success) { return checkResult; } } } // We have already reserved enough resources for read requests, and have granted enough resources for current subscriptions, so // we should be able to allocate resources requested by this request. ReadHandler * handler = mReadHandlers.CreateObject(*this, apExchangeContext, aInteractionType, mReportScheduler, GetDataModelProvider()); if (handler == nullptr) { ChipLogProgress(InteractionModel, "no resource for %s interaction", aInteractionType == ReadHandler::InteractionType::Subscribe ? "Subscribe" : "Read"); return Status::ResourceExhausted; } handler->OnInitialRequest(std::move(aPayload)); return Status::Success; } Protocols::InteractionModel::Status InteractionModelEngine::OnWriteRequest(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload, bool aIsTimedWrite) { ChipLogDetail(InteractionModel, "Received Write request"); for (auto & writeHandler : mWriteHandlers) { if (writeHandler.IsFree()) { VerifyOrReturnError(writeHandler.Init(GetDataModelProvider(), this) == CHIP_NO_ERROR, Status::Busy); return writeHandler.OnWriteRequest(apExchangeContext, std::move(aPayload), aIsTimedWrite); } } ChipLogProgress(InteractionModel, "no resource for write interaction"); return Status::Busy; } CHIP_ERROR InteractionModelEngine::OnTimedRequest(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload, Protocols::InteractionModel::Status & aStatus) { TimedHandler * handler = mTimedHandlers.CreateObject(this); if (handler == nullptr) { ChipLogProgress(InteractionModel, "no resource for Timed interaction"); aStatus = Status::Busy; return CHIP_ERROR_NO_MEMORY; } // The timed handler takes over handling of this exchange and will do its // own status reporting as needed. aStatus = Status::Success; apExchangeContext->SetDelegate(handler); return handler->OnMessageReceived(apExchangeContext, aPayloadHeader, std::move(aPayload)); } #if CHIP_CONFIG_ENABLE_READ_CLIENT Status InteractionModelEngine::OnUnsolicitedReportData(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) { System::PacketBufferTLVReader reader; reader.Init(aPayload.Retain()); ReportDataMessage::Parser report; VerifyOrReturnError(report.Init(reader) == CHIP_NO_ERROR, Status::InvalidAction); #if CHIP_CONFIG_IM_PRETTY_PRINT report.PrettyPrint(); #endif SubscriptionId subscriptionId = 0; VerifyOrReturnError(report.GetSubscriptionId(&subscriptionId) == CHIP_NO_ERROR, Status::InvalidAction); VerifyOrReturnError(report.ExitContainer() == CHIP_NO_ERROR, Status::InvalidAction); ReadClient * foundSubscription = nullptr; for (auto * readClient = mpActiveReadClientList; readClient != nullptr; readClient = readClient->GetNextClient()) { auto peer = apExchangeContext->GetSessionHandle()->GetPeer(); if (readClient->GetFabricIndex() != peer.GetFabricIndex() || readClient->GetPeerNodeId() != peer.GetNodeId()) { continue; } // Notify Subscriptions about incoming communication from node readClient->OnUnsolicitedMessageFromPublisher(); if (!readClient->IsSubscriptionActive()) { continue; } if (!readClient->IsMatchingSubscriptionId(subscriptionId)) { continue; } if (!foundSubscription) { foundSubscription = readClient; } } if (foundSubscription) { foundSubscription->OnUnsolicitedReportData(apExchangeContext, std::move(aPayload)); return Status::Success; } ChipLogDetail(InteractionModel, "Received report with invalid subscriptionId %" PRIu32, subscriptionId); return Status::InvalidSubscription; } #endif // CHIP_CONFIG_ENABLE_READ_CLIENT CHIP_ERROR InteractionModelEngine::OnUnsolicitedMessageReceived(const PayloadHeader & payloadHeader, ExchangeDelegate *& newDelegate) { // TODO: Implement OnUnsolicitedMessageReceived, let messaging layer dispatch message to ReadHandler/ReadClient/TimedHandler // directly. newDelegate = this; return CHIP_NO_ERROR; } CHIP_ERROR InteractionModelEngine::OnMessageReceived(Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) { using namespace Protocols::InteractionModel; Protocols::InteractionModel::Status status = Status::Failure; // Ensure that DataModel::Provider has access to the exchange the message was received on. CurrentExchangeValueScope scopedExchangeContext(*this, apExchangeContext); // Group Message can only be an InvokeCommandRequest or WriteRequest if (apExchangeContext->IsGroupExchangeContext() && !aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::InvokeCommandRequest) && !aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::WriteRequest)) { ChipLogProgress(InteractionModel, "Msg type %d not supported for group message", aPayloadHeader.GetMessageType()); return CHIP_NO_ERROR; } if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::InvokeCommandRequest)) { status = OnInvokeCommandRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), /* aIsTimedInvoke = */ false); } else if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::ReadRequest)) { status = OnReadInitialRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), ReadHandler::InteractionType::Read); } else if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::WriteRequest)) { status = OnWriteRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), /* aIsTimedWrite = */ false); } else if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::SubscribeRequest)) { status = OnReadInitialRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), ReadHandler::InteractionType::Subscribe); } #if CHIP_CONFIG_ENABLE_READ_CLIENT else if (aPayloadHeader.HasMessageType(Protocols::InteractionModel::MsgType::ReportData)) { status = OnUnsolicitedReportData(apExchangeContext, aPayloadHeader, std::move(aPayload)); } #endif // CHIP_CONFIG_ENABLE_READ_CLIENT else if (aPayloadHeader.HasMessageType(MsgType::TimedRequest)) { OnTimedRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), status); } else { ChipLogProgress(InteractionModel, "Msg type %d not supported", aPayloadHeader.GetMessageType()); status = Status::InvalidAction; } if (status != Status::Success && !apExchangeContext->IsGroupExchangeContext()) { return StatusResponse::Send(status, apExchangeContext, false /*aExpectResponse*/); } return CHIP_NO_ERROR; } void InteractionModelEngine::OnResponseTimeout(Messaging::ExchangeContext * ec) { ChipLogError(InteractionModel, "Time out! Failed to receive IM response from Exchange: " ChipLogFormatExchange, ChipLogValueExchange(ec)); } #if CHIP_CONFIG_ENABLE_READ_CLIENT void InteractionModelEngine::OnActiveModeNotification(ScopedNodeId aPeer) { for (ReadClient * pListItem = mpActiveReadClientList; pListItem != nullptr;) { auto pNextItem = pListItem->GetNextClient(); // It is possible that pListItem is destroyed by the app in OnActiveModeNotification. // Get the next item before invoking `OnActiveModeNotification`. if (ScopedNodeId(pListItem->GetPeerNodeId(), pListItem->GetFabricIndex()) == aPeer) { pListItem->OnActiveModeNotification(); } pListItem = pNextItem; } } void InteractionModelEngine::OnPeerTypeChange(ScopedNodeId aPeer, ReadClient::PeerType aType) { // TODO: Follow up to use a iterator function to avoid copy/paste here. for (ReadClient * pListItem = mpActiveReadClientList; pListItem != nullptr;) { // It is possible that pListItem is destroyed by the app in OnPeerTypeChange. // Get the next item before invoking `OnPeerTypeChange`. auto pNextItem = pListItem->GetNextClient(); if (ScopedNodeId(pListItem->GetPeerNodeId(), pListItem->GetFabricIndex()) == aPeer) { pListItem->OnPeerTypeChange(aType); } pListItem = pNextItem; } } void InteractionModelEngine::AddReadClient(ReadClient * apReadClient) { apReadClient->SetNextClient(mpActiveReadClientList); mpActiveReadClientList = apReadClient; } #endif // CHIP_CONFIG_ENABLE_READ_CLIENT bool InteractionModelEngine::TrimFabricForSubscriptions(FabricIndex aFabricIndex, bool aForceEvict) { const size_t pathPoolCapacity = GetPathPoolCapacityForSubscriptions(); const size_t readHandlerPoolCapacity = GetReadHandlerPoolCapacityForSubscriptions(); uint8_t fabricCount = mpFabricTable->FabricCount(); size_t attributePathsSubscribedByCurrentFabric = 0; size_t eventPathsSubscribedByCurrentFabric = 0; size_t subscriptionsEstablishedByCurrentFabric = 0; if (fabricCount == 0) { return false; } // Note: This is OK only when we have assumed the fabricCount is not zero. Should be revised when adding support to // subscriptions on PASE sessions. size_t perFabricPathCapacity = pathPoolCapacity / static_cast(fabricCount); size_t perFabricSubscriptionCapacity = readHandlerPoolCapacity / static_cast(fabricCount); ReadHandler * candidate = nullptr; size_t candidateAttributePathsUsed = 0; size_t candidateEventPathsUsed = 0; // It is safe to use & here since this function will be called on current stack. mReadHandlers.ForEachActiveObject([&](ReadHandler * handler) { if (handler->GetAccessingFabricIndex() != aFabricIndex || !handler->IsType(ReadHandler::InteractionType::Subscribe)) { return Loop::Continue; } size_t attributePathsUsed = handler->GetAttributePathCount(); size_t eventPathsUsed = handler->GetEventPathCount(); attributePathsSubscribedByCurrentFabric += attributePathsUsed; eventPathsSubscribedByCurrentFabric += eventPathsUsed; subscriptionsEstablishedByCurrentFabric++; if (candidate == nullptr) { candidate = handler; } // This handler uses more resources than the one we picked before. else if ((attributePathsUsed > perFabricPathCapacity || eventPathsUsed > perFabricPathCapacity) && (candidateAttributePathsUsed <= perFabricPathCapacity && candidateEventPathsUsed <= perFabricPathCapacity)) { candidate = handler; candidateAttributePathsUsed = attributePathsUsed; candidateEventPathsUsed = eventPathsUsed; } // This handler is older than the one we picked before. else if (handler->GetTransactionStartGeneration() < candidate->GetTransactionStartGeneration() && // And the level of resource usage is the same (both exceed or neither exceed) ((attributePathsUsed > perFabricPathCapacity || eventPathsUsed > perFabricPathCapacity) == (candidateAttributePathsUsed > perFabricPathCapacity || candidateEventPathsUsed > perFabricPathCapacity))) { candidate = handler; } return Loop::Continue; }); if (candidate != nullptr && (aForceEvict || attributePathsSubscribedByCurrentFabric > perFabricPathCapacity || eventPathsSubscribedByCurrentFabric > perFabricPathCapacity || subscriptionsEstablishedByCurrentFabric > perFabricSubscriptionCapacity)) { SubscriptionId subId; candidate->GetSubscriptionId(subId); ChipLogProgress(DataManagement, "Evicting Subscription ID %u:0x%" PRIx32, candidate->GetSubjectDescriptor().fabricIndex, subId); candidate->Close(); return true; } return false; } bool InteractionModelEngine::EnsureResourceForSubscription(FabricIndex aFabricIndex, size_t aRequestedAttributePathCount, size_t aRequestedEventPathCount) { #if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP && !CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK #if CONFIG_BUILD_FOR_HOST_UNIT_TEST const bool allowUnlimited = !mForceHandlerQuota; #else // CONFIG_BUILD_FOR_HOST_UNIT_TEST // If the resources are allocated on the heap, we should be able to handle as many Read / Subscribe requests as possible. const bool allowUnlimited = true; #endif // CONFIG_BUILD_FOR_HOST_UNIT_TEST #else // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP && !CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK const bool allowUnlimited = false; #endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP && !CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK // Don't couple with read requests, always reserve enough resource for read requests. const size_t pathPoolCapacity = GetPathPoolCapacityForSubscriptions(); const size_t readHandlerPoolCapacity = GetReadHandlerPoolCapacityForSubscriptions(); // If we return early here, the compiler will complain about the unreachable code, so we add a always-true check. const size_t attributePathCap = allowUnlimited ? SIZE_MAX : pathPoolCapacity; const size_t eventPathCap = allowUnlimited ? SIZE_MAX : pathPoolCapacity; const size_t readHandlerCap = allowUnlimited ? SIZE_MAX : readHandlerPoolCapacity; size_t usedAttributePaths = 0; size_t usedEventPaths = 0; size_t usedReadHandlers = 0; auto countResourceUsage = [&]() { usedAttributePaths = 0; usedEventPaths = 0; usedReadHandlers = 0; mReadHandlers.ForEachActiveObject([&](auto * handler) { if (!handler->IsType(ReadHandler::InteractionType::Subscribe)) { return Loop::Continue; } usedAttributePaths += handler->GetAttributePathCount(); usedEventPaths += handler->GetEventPathCount(); usedReadHandlers++; return Loop::Continue; }); }; countResourceUsage(); if (usedAttributePaths + aRequestedAttributePathCount <= attributePathCap && usedEventPaths + aRequestedEventPathCount <= eventPathCap && usedReadHandlers < readHandlerCap) { // We have enough resources, then we serve the requests in a best-effort manner. return true; } if ((aRequestedAttributePathCount > kMinSupportedPathsPerSubscription && usedAttributePaths + aRequestedAttributePathCount > attributePathCap) || (aRequestedEventPathCount > kMinSupportedPathsPerSubscription && usedEventPaths + aRequestedEventPathCount > eventPathCap)) { // We cannot offer enough resources, and the subscription is requesting more than the spec limit. return false; } const auto evictAndUpdateResourceUsage = [&](FabricIndex fabricIndex, bool forceEvict) { bool ret = TrimFabricForSubscriptions(fabricIndex, forceEvict); countResourceUsage(); return ret; }; // // At this point, we have an inbound request that respects minimas but we still don't have enough resources to handle it. Which // means that we definitely have handlers on existing fabrics that are over limits and need to evict at least one of them to // make space. // // There might be cases that one fabric has lots of subscriptions with one interested path, while the other fabrics are not // using excess resources. So we need to do this multiple times until we have enough room or no fabrics are using excess // resources. // bool didEvictHandler = true; while (didEvictHandler) { didEvictHandler = false; for (const auto & fabric : *mpFabricTable) { // The resources are enough to serve this request, do not evict anything. if (usedAttributePaths + aRequestedAttributePathCount <= attributePathCap && usedEventPaths + aRequestedEventPathCount <= eventPathCap && usedReadHandlers < readHandlerCap) { break; } didEvictHandler = didEvictHandler || evictAndUpdateResourceUsage(fabric.GetFabricIndex(), false); } } // The above loop cannot guarantee the resources for the new subscriptions when the resource usage from all fabrics are exactly // within the quota (which means we have exactly used all resources). Evict (from the large subscriptions first then from // oldest) subscriptions from the current fabric until we have enough resource for the new subscription. didEvictHandler = true; while ((usedAttributePaths + aRequestedAttributePathCount > attributePathCap || usedEventPaths + aRequestedEventPathCount > eventPathCap || usedReadHandlers >= readHandlerCap) && // Avoid infinity loop didEvictHandler) { didEvictHandler = evictAndUpdateResourceUsage(aFabricIndex, true); } // If didEvictHandler is false, means the loop above evicted all subscriptions from the current fabric but we still don't have // enough resources for the new subscription, this should never happen. // This is safe as long as we have rejected subscriptions without a fabric associated (with a PASE session) before. // Note: Spec#5141: should reject subscription requests on PASE sessions. VerifyOrDieWithMsg(didEvictHandler, DataManagement, "Failed to get required resources by evicting existing subscriptions."); // We have ensured enough resources by the logic above. return true; } bool InteractionModelEngine::TrimFabricForRead(FabricIndex aFabricIndex) { const size_t guaranteedReadRequestsPerFabric = GetGuaranteedReadRequestsPerFabric(); const size_t minSupportedPathsPerFabricForRead = guaranteedReadRequestsPerFabric * kMinSupportedPathsPerReadRequest; size_t attributePathsUsedByCurrentFabric = 0; size_t eventPathsUsedByCurrentFabric = 0; size_t readTransactionsOnCurrentFabric = 0; ReadHandler * candidate = nullptr; size_t candidateAttributePathsUsed = 0; size_t candidateEventPathsUsed = 0; // It is safe to use & here since this function will be called on current stack. mReadHandlers.ForEachActiveObject([&](ReadHandler * handler) { if (handler->GetAccessingFabricIndex() != aFabricIndex || !handler->IsType(ReadHandler::InteractionType::Read)) { return Loop::Continue; } size_t attributePathsUsed = handler->GetAttributePathCount(); size_t eventPathsUsed = handler->GetEventPathCount(); attributePathsUsedByCurrentFabric += attributePathsUsed; eventPathsUsedByCurrentFabric += eventPathsUsed; readTransactionsOnCurrentFabric++; if (candidate == nullptr) { candidate = handler; } // Oversized read handlers will be evicted first. else if ((attributePathsUsed > kMinSupportedPathsPerReadRequest || eventPathsUsed > kMinSupportedPathsPerReadRequest) && (candidateAttributePathsUsed <= kMinSupportedPathsPerReadRequest && candidateEventPathsUsed <= kMinSupportedPathsPerReadRequest)) { candidate = handler; } // Read Handlers are "first come first served", so we give eariler read transactions a higher priority. else if (handler->GetTransactionStartGeneration() > candidate->GetTransactionStartGeneration() && // And the level of resource usage is the same (both exceed or neither exceed) ((attributePathsUsed > kMinSupportedPathsPerReadRequest || eventPathsUsed > kMinSupportedPathsPerReadRequest) == (candidateAttributePathsUsed > kMinSupportedPathsPerReadRequest || candidateEventPathsUsed > kMinSupportedPathsPerReadRequest))) { candidate = handler; } if (candidate == handler) { candidateAttributePathsUsed = attributePathsUsed; candidateEventPathsUsed = eventPathsUsed; } return Loop::Continue; }); if (candidate != nullptr && ((attributePathsUsedByCurrentFabric > minSupportedPathsPerFabricForRead || eventPathsUsedByCurrentFabric > minSupportedPathsPerFabricForRead || readTransactionsOnCurrentFabric > guaranteedReadRequestsPerFabric) || // Always evict the transactions on PASE sessions if the fabric table is full. (aFabricIndex == kUndefinedFabricIndex && mpFabricTable->FabricCount() == GetConfigMaxFabrics()))) { candidate->Close(); return true; } return false; } Protocols::InteractionModel::Status InteractionModelEngine::EnsureResourceForRead(FabricIndex aFabricIndex, size_t aRequestedAttributePathCount, size_t aRequestedEventPathCount) { #if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP && !CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK #if CONFIG_BUILD_FOR_HOST_UNIT_TEST const bool allowUnlimited = !mForceHandlerQuota; #else // CONFIG_BUILD_FOR_HOST_UNIT_TEST // If the resources are allocated on the heap, we should be able to handle as many Read / Subscribe requests as possible. const bool allowUnlimited = true; #endif // CONFIG_BUILD_FOR_HOST_UNIT_TEST #else // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP && !CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK const bool allowUnlimited = false; #endif // CHIP_SYSTEM_CONFIG_POOL_USE_HEAP && !CHIP_CONFIG_IM_FORCE_FABRIC_QUOTA_CHECK // If we return early here, the compiler will complain about the unreachable code, so we add a always-true check. const size_t attributePathCap = allowUnlimited ? SIZE_MAX : GetPathPoolCapacityForReads(); const size_t eventPathCap = allowUnlimited ? SIZE_MAX : GetPathPoolCapacityForReads(); const size_t readHandlerCap = allowUnlimited ? SIZE_MAX : GetReadHandlerPoolCapacityForReads(); const size_t guaranteedReadRequestsPerFabric = GetGuaranteedReadRequestsPerFabric(); const size_t guaranteedPathsPerFabric = kMinSupportedPathsPerReadRequest * guaranteedReadRequestsPerFabric; size_t usedAttributePaths = 0; size_t usedEventPaths = 0; size_t usedReadHandlers = 0; auto countResourceUsage = [&]() { usedAttributePaths = 0; usedEventPaths = 0; usedReadHandlers = 0; mReadHandlers.ForEachActiveObject([&](auto * handler) { if (!handler->IsType(ReadHandler::InteractionType::Read)) { return Loop::Continue; } usedAttributePaths += handler->GetAttributePathCount(); usedEventPaths += handler->GetEventPathCount(); usedReadHandlers++; return Loop::Continue; }); }; auto haveEnoughResourcesForTheRequest = [&]() { return usedAttributePaths + aRequestedAttributePathCount <= attributePathCap && usedEventPaths + aRequestedEventPathCount <= eventPathCap && usedReadHandlers < readHandlerCap; }; countResourceUsage(); if (haveEnoughResourcesForTheRequest()) { // We have enough resources, then we serve the requests in a best-effort manner. return Status::Success; } if ((aRequestedAttributePathCount > kMinSupportedPathsPerReadRequest && usedAttributePaths + aRequestedAttributePathCount > attributePathCap) || (aRequestedEventPathCount > kMinSupportedPathsPerReadRequest && usedEventPaths + aRequestedEventPathCount > eventPathCap)) { // We cannot offer enough resources, and the read transaction is requesting more than the spec limit. return Status::PathsExhausted; } // If we have commissioned CHIP_CONFIG_MAX_FABRICS already, and this transaction doesn't have an associated fabric index, reject // the request if we don't have sufficient resources for this request. if (mpFabricTable->FabricCount() == GetConfigMaxFabrics() && aFabricIndex == kUndefinedFabricIndex) { return Status::Busy; } size_t usedAttributePathsInFabric = 0; size_t usedEventPathsInFabric = 0; size_t usedReadHandlersInFabric = 0; mReadHandlers.ForEachActiveObject([&](auto * handler) { if (!handler->IsType(ReadHandler::InteractionType::Read) || handler->GetAccessingFabricIndex() != aFabricIndex) { return Loop::Continue; } usedAttributePathsInFabric += handler->GetAttributePathCount(); usedEventPathsInFabric += handler->GetEventPathCount(); usedReadHandlersInFabric++; return Loop::Continue; }); // Busy, since there are already some read requests ongoing on this fabric, please retry later. if (usedAttributePathsInFabric + aRequestedAttributePathCount > guaranteedPathsPerFabric || usedEventPathsInFabric + aRequestedEventPathCount > guaranteedPathsPerFabric || usedReadHandlersInFabric >= guaranteedReadRequestsPerFabric) { return Status::Busy; } const auto evictAndUpdateResourceUsage = [&](FabricIndex fabricIndex) { bool ret = TrimFabricForRead(fabricIndex); countResourceUsage(); return ret; }; // // At this point, we have an inbound request that respects minimas but we still don't have enough resources to handle it. Which // means that we definitely have handlers on existing fabrics that are over limits and need to evict at least one of them to // make space. // bool didEvictHandler = true; while (didEvictHandler) { didEvictHandler = false; didEvictHandler = didEvictHandler || evictAndUpdateResourceUsage(kUndefinedFabricIndex); if (haveEnoughResourcesForTheRequest()) { break; } // If the fabric table is full, we won't evict read requests from normal fabrics before we have evicted all read requests // from PASE sessions. if (mpFabricTable->FabricCount() == GetConfigMaxFabrics() && didEvictHandler) { continue; } for (const auto & fabric : *mpFabricTable) { didEvictHandler = didEvictHandler || evictAndUpdateResourceUsage(fabric.GetFabricIndex()); // If we now have enough resources to serve this request, stop evicting things. if (haveEnoughResourcesForTheRequest()) { break; } } } // Now all fabrics are not oversized (since we have trimmed the oversized fabrics in the loop above), and the read handler is // also not oversized, we should be able to handle this read transaction. VerifyOrDie(haveEnoughResourcesForTheRequest()); return Status::Success; } #if CHIP_CONFIG_ENABLE_READ_CLIENT void InteractionModelEngine::RemoveReadClient(ReadClient * apReadClient) { ReadClient * pPrevListItem = nullptr; ReadClient * pCurListItem = mpActiveReadClientList; while (pCurListItem != apReadClient) { // // Item must exist in this tracker list. If not, there's a bug somewhere. // VerifyOrDie(pCurListItem != nullptr); pPrevListItem = pCurListItem; pCurListItem = pCurListItem->GetNextClient(); } if (pPrevListItem) { pPrevListItem->SetNextClient(apReadClient->GetNextClient()); } else { mpActiveReadClientList = apReadClient->GetNextClient(); } apReadClient->SetNextClient(nullptr); } size_t InteractionModelEngine::GetNumActiveReadClients() { ReadClient * pListItem = mpActiveReadClientList; size_t count = 0; while (pListItem) { pListItem = pListItem->GetNextClient(); count++; } return count; } bool InteractionModelEngine::InActiveReadClientList(ReadClient * apReadClient) { ReadClient * pListItem = mpActiveReadClientList; while (pListItem) { if (pListItem == apReadClient) { return true; } pListItem = pListItem->GetNextClient(); } return false; } #endif // CHIP_CONFIG_ENABLE_READ_CLIENT bool InteractionModelEngine::HasConflictWriteRequests(const WriteHandler * apWriteHandler, const ConcreteAttributePath & aPath) { for (auto & writeHandler : mWriteHandlers) { if (writeHandler.IsFree() || &writeHandler == apWriteHandler) { continue; } if (writeHandler.IsCurrentlyProcessingWritePath(aPath)) { return true; } } return false; } void InteractionModelEngine::ReleaseAttributePathList(SingleLinkedListNode *& aAttributePathList) { ReleasePool(aAttributePathList, mAttributePathPool); } CHIP_ERROR InteractionModelEngine::PushFrontAttributePathList(SingleLinkedListNode *& aAttributePathList, AttributePathParams & aAttributePath) { CHIP_ERROR err = PushFront(aAttributePathList, aAttributePath, mAttributePathPool); if (err == CHIP_ERROR_NO_MEMORY) { ChipLogError(InteractionModel, "AttributePath pool full"); return CHIP_IM_GLOBAL_STATUS(PathsExhausted); } return err; } bool InteractionModelEngine::IsExistentAttributePath(const ConcreteAttributePath & path) { #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE #if CHIP_CONFIG_USE_EMBER_DATA_MODEL bool providerResult = GetDataModelProvider() ->GetAttributeInfo(ConcreteAttributePath(path.mEndpointId, path.mClusterId, path.mAttributeId)) .has_value(); bool emberResult = emberAfContainsAttribute(path.mEndpointId, path.mClusterId, path.mAttributeId); // Ensure that Provider interface and ember are IDENTICAL in attribute location (i.e. "check" mode) VerifyOrDie(providerResult == emberResult); #endif return GetDataModelProvider() ->GetAttributeInfo(ConcreteAttributePath(path.mEndpointId, path.mClusterId, path.mAttributeId)) .has_value(); #else return emberAfContainsAttribute(path.mEndpointId, path.mClusterId, path.mAttributeId); #endif } void InteractionModelEngine::RemoveDuplicateConcreteAttributePath(SingleLinkedListNode *& aAttributePaths) { SingleLinkedListNode * prev = nullptr; auto * path1 = aAttributePaths; while (path1 != nullptr) { bool duplicate = false; // skip all wildcard paths and invalid concrete attribute if (path1->mValue.IsWildcardPath() || !IsExistentAttributePath( ConcreteAttributePath(path1->mValue.mEndpointId, path1->mValue.mClusterId, path1->mValue.mAttributeId))) { prev = path1; path1 = path1->mpNext; continue; } // Check whether a wildcard path expands to something that includes this concrete path. for (auto * path2 = aAttributePaths; path2 != nullptr; path2 = path2->mpNext) { if (path2 == path1) { continue; } if (path2->mValue.IsWildcardPath() && path2->mValue.IsAttributePathSupersetOf(path1->mValue)) { duplicate = true; break; } } // if path1 duplicates something from wildcard expansion, discard path1 if (!duplicate) { prev = path1; path1 = path1->mpNext; continue; } if (path1 == aAttributePaths) { aAttributePaths = path1->mpNext; mAttributePathPool.ReleaseObject(path1); path1 = aAttributePaths; } else { prev->mpNext = path1->mpNext; mAttributePathPool.ReleaseObject(path1); path1 = prev->mpNext; } } } void InteractionModelEngine::ReleaseEventPathList(SingleLinkedListNode *& aEventPathList) { ReleasePool(aEventPathList, mEventPathPool); } CHIP_ERROR InteractionModelEngine::PushFrontEventPathParamsList(SingleLinkedListNode *& aEventPathList, EventPathParams & aEventPath) { CHIP_ERROR err = PushFront(aEventPathList, aEventPath, mEventPathPool); if (err == CHIP_ERROR_NO_MEMORY) { ChipLogError(InteractionModel, "EventPath pool full"); return CHIP_IM_GLOBAL_STATUS(PathsExhausted); } return err; } void InteractionModelEngine::ReleaseDataVersionFilterList(SingleLinkedListNode *& aDataVersionFilterList) { ReleasePool(aDataVersionFilterList, mDataVersionFilterPool); } CHIP_ERROR InteractionModelEngine::PushFrontDataVersionFilterList(SingleLinkedListNode *& aDataVersionFilterList, DataVersionFilter & aDataVersionFilter) { CHIP_ERROR err = PushFront(aDataVersionFilterList, aDataVersionFilter, mDataVersionFilterPool); if (err == CHIP_ERROR_NO_MEMORY) { ChipLogError(InteractionModel, "DataVersionFilter pool full, ignore this filter"); err = CHIP_NO_ERROR; } return err; } template void InteractionModelEngine::ReleasePool(SingleLinkedListNode *& aObjectList, ObjectPool, N> & aObjectPool) { SingleLinkedListNode * current = aObjectList; while (current != nullptr) { SingleLinkedListNode * nextObject = current->mpNext; aObjectPool.ReleaseObject(current); current = nextObject; } aObjectList = nullptr; } template CHIP_ERROR InteractionModelEngine::PushFront(SingleLinkedListNode *& aObjectList, T & aData, ObjectPool, N> & aObjectPool) { SingleLinkedListNode * object = aObjectPool.CreateObject(); if (object == nullptr) { return CHIP_ERROR_NO_MEMORY; } object->mValue = aData; object->mpNext = aObjectList; aObjectList = object; return CHIP_NO_ERROR; } void InteractionModelEngine::DispatchCommand(CommandHandlerImpl & apCommandObj, const ConcreteCommandPath & aCommandPath, TLV::TLVReader & apPayload) { #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE DataModel::InvokeRequest request; request.path = aCommandPath; request.invokeFlags.Set(DataModel::InvokeFlags::kTimed, apCommandObj.IsTimedInvoke()); request.subjectDescriptor = apCommandObj.GetSubjectDescriptor(); std::optional status = GetDataModelProvider()->Invoke(request, apPayload, &apCommandObj); // Provider indicates that handler status or data was already set (or will be set asynchronously) by // returning std::nullopt. If any other value is returned, it is requesting that a status is set. This // includes CHIP_NO_ERROR: in this case CHIP_NO_ERROR would mean set a `status success on the command` if (status.has_value()) { apCommandObj.AddStatus(aCommandPath, status->GetStatusCode()); } #else CommandHandlerInterface * handler = CommandHandlerInterfaceRegistry::Instance().GetCommandHandler(aCommandPath.mEndpointId, aCommandPath.mClusterId); if (handler) { CommandHandlerInterface::HandlerContext context(apCommandObj, aCommandPath, apPayload); handler->InvokeCommand(context); // // If the command was handled, don't proceed any further and return successfully. // if (context.mCommandHandled) { return; } } DispatchSingleClusterCommand(aCommandPath, apPayload, &apCommandObj); #endif // CHIP_CONFIG_USE_DATA_MODEL_INTERFACE } Protocols::InteractionModel::Status InteractionModelEngine::ValidateCommandCanBeDispatched(const DataModel::InvokeRequest & request) { Status status = CheckCommandExistence(request.path); if (status != Status::Success) { ChipLogDetail(DataManagement, "No command " ChipLogFormatMEI " in Cluster " ChipLogFormatMEI " on Endpoint %u", ChipLogValueMEI(request.path.mCommandId), ChipLogValueMEI(request.path.mClusterId), request.path.mEndpointId); return status; } status = CheckCommandAccess(request); VerifyOrReturnValue(status == Status::Success, status); return CheckCommandFlags(request); } Protocols::InteractionModel::Status InteractionModelEngine::CheckCommandAccess(const DataModel::InvokeRequest & aRequest) { if (!aRequest.subjectDescriptor.has_value()) { return Status::UnsupportedAccess; // we require a subject for invoke } Access::RequestPath requestPath{ .cluster = aRequest.path.mClusterId, .endpoint = aRequest.path.mEndpointId, .requestType = Access::RequestType::kCommandInvokeRequest, .entityId = aRequest.path.mCommandId }; #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE std::optional commandInfo = mDataModelProvider->GetAcceptedCommandInfo(aRequest.path); Access::Privilege minimumRequiredPrivilege = commandInfo.has_value() ? commandInfo->invokePrivilege : Access::Privilege::kOperate; #else Access::Privilege minimumRequiredPrivilege = RequiredPrivilege::ForInvokeCommand(aRequest.path); #endif CHIP_ERROR err = Access::GetAccessControl().Check(*aRequest.subjectDescriptor, requestPath, minimumRequiredPrivilege); if (err != CHIP_NO_ERROR) { if ((err != CHIP_ERROR_ACCESS_DENIED) && (err != CHIP_ERROR_ACCESS_RESTRICTED_BY_ARL)) { return Status::Failure; } return err == CHIP_ERROR_ACCESS_DENIED ? Status::UnsupportedAccess : Status::AccessRestricted; } return Status::Success; } Protocols::InteractionModel::Status InteractionModelEngine::CheckCommandFlags(const DataModel::InvokeRequest & aRequest) { #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE std::optional commandInfo = mDataModelProvider->GetAcceptedCommandInfo(aRequest.path); // This is checked by previous validations, so it should not happen VerifyOrDie(commandInfo.has_value()); const bool commandNeedsTimedInvoke = commandInfo->flags.Has(DataModel::CommandQualityFlags::kTimed); const bool commandIsFabricScoped = commandInfo->flags.Has(DataModel::CommandQualityFlags::kFabricScoped); #else const bool commandNeedsTimedInvoke = CommandNeedsTimedInvoke(aRequest.path.mClusterId, aRequest.path.mCommandId); const bool commandIsFabricScoped = CommandIsFabricScoped(aRequest.path.mClusterId, aRequest.path.mCommandId); #endif if (commandNeedsTimedInvoke && !aRequest.invokeFlags.Has(DataModel::InvokeFlags::kTimed)) { return Status::NeedsTimedInteraction; } if (commandIsFabricScoped) { // 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 (aRequest.GetAccessingFabricIndex() == kUndefinedFabricIndex) { return Status::UnsupportedAccess; } } return Status::Success; } Protocols::InteractionModel::Status InteractionModelEngine::CheckCommandExistence(const ConcreteCommandPath & aCommandPath) { #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE auto provider = GetDataModelProvider(); if (provider->GetAcceptedCommandInfo(aCommandPath).has_value()) { #if CHIP_CONFIG_USE_EMBER_DATA_MODEL VerifyOrDie(ServerClusterCommandExists(aCommandPath) == Protocols::InteractionModel::Status::Success); #endif return Protocols::InteractionModel::Status::Success; } // We failed, figure out why ... // if (provider->GetClusterInfo(aCommandPath).has_value()) { #if CHIP_CONFIG_USE_EMBER_DATA_MODEL VerifyOrDie(ServerClusterCommandExists(aCommandPath) == Protocols::InteractionModel::Status::UnsupportedCommand); #endif return Protocols::InteractionModel::Status::UnsupportedCommand; // cluster exists, so command is invalid } // At this point either cluster or endpoint does not exist. If we find the endpoint, then the cluster // is invalid for (EndpointId endpoint = provider->FirstEndpoint(); endpoint != kInvalidEndpointId; endpoint = provider->NextEndpoint(endpoint)) { if (endpoint == aCommandPath.mEndpointId) { #if CHIP_CONFIG_USE_EMBER_DATA_MODEL VerifyOrDie(ServerClusterCommandExists(aCommandPath) == Protocols::InteractionModel::Status::UnsupportedCluster); #endif // endpoint exists, so cluster is invalid return Protocols::InteractionModel::Status::UnsupportedCluster; } } // endpoint not found #if CHIP_CONFIG_USE_EMBER_DATA_MODEL VerifyOrDie(ServerClusterCommandExists(aCommandPath) == Protocols::InteractionModel::Status::UnsupportedEndpoint); #endif return Protocols::InteractionModel::Status::UnsupportedEndpoint; #else return ServerClusterCommandExists(aCommandPath); #endif } DataModel::Provider * InteractionModelEngine::SetDataModelProvider(DataModel::Provider * model) { // Alternting data model should not be done while IM is actively handling requests. VerifyOrDie(mReadHandlers.begin() == mReadHandlers.end()); DataModel::Provider * oldModel = mDataModelProvider; if (oldModel != nullptr) { CHIP_ERROR err = oldModel->Shutdown(); if (err != CHIP_NO_ERROR) { ChipLogError(InteractionModel, "Failure on interaction model shutdown: %" CHIP_ERROR_FORMAT, err.Format()); } } mDataModelProvider = model; if (mDataModelProvider != nullptr) { DataModel::InteractionModelContext context; context.eventsGenerator = &EventManagement::GetInstance(); context.dataModelChangeListener = &mReportingEngine; context.actionContext = this; CHIP_ERROR err = mDataModelProvider->Startup(context); if (err != CHIP_NO_ERROR) { ChipLogError(InteractionModel, "Failure on interaction model startup: %" CHIP_ERROR_FORMAT, err.Format()); } } return oldModel; } DataModel::Provider * InteractionModelEngine::GetDataModelProvider() { #if CHIP_CONFIG_USE_DATA_MODEL_INTERFACE if (mDataModelProvider == nullptr) { // These should be called within the CHIP processing loop. assertChipStackLockedByCurrentThread(); SetDataModelProvider(CodegenDataModelProviderInstance()); } #endif return mDataModelProvider; } void InteractionModelEngine::OnTimedInteractionFailed(TimedHandler * apTimedHandler) { mTimedHandlers.ReleaseObject(apTimedHandler); } void InteractionModelEngine::OnTimedInvoke(TimedHandler * apTimedHandler, Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) { using namespace Protocols::InteractionModel; // Reset the ourselves as the exchange delegate for now, to match what we'd // do with an initial unsolicited invoke. apExchangeContext->SetDelegate(this); mTimedHandlers.ReleaseObject(apTimedHandler); VerifyOrDie(aPayloadHeader.HasMessageType(MsgType::InvokeCommandRequest)); VerifyOrDie(!apExchangeContext->IsGroupExchangeContext()); Status status = OnInvokeCommandRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), /* aIsTimedInvoke = */ true); if (status != Status::Success) { StatusResponse::Send(status, apExchangeContext, /* aExpectResponse = */ false); } } void InteractionModelEngine::OnTimedWrite(TimedHandler * apTimedHandler, Messaging::ExchangeContext * apExchangeContext, const PayloadHeader & aPayloadHeader, System::PacketBufferHandle && aPayload) { using namespace Protocols::InteractionModel; // Reset the ourselves as the exchange delegate for now, to match what we'd // do with an initial unsolicited write. apExchangeContext->SetDelegate(this); mTimedHandlers.ReleaseObject(apTimedHandler); VerifyOrDie(aPayloadHeader.HasMessageType(MsgType::WriteRequest)); VerifyOrDie(!apExchangeContext->IsGroupExchangeContext()); Status status = OnWriteRequest(apExchangeContext, aPayloadHeader, std::move(aPayload), /* aIsTimedWrite = */ true); if (status != Status::Success) { StatusResponse::Send(status, apExchangeContext, /* aExpectResponse = */ false); } } bool InteractionModelEngine::HasActiveRead() { return ((mReadHandlers.ForEachActiveObject([](ReadHandler * handler) { if (handler->IsType(ReadHandler::InteractionType::Read)) { return Loop::Break; } return Loop::Continue; }) == Loop::Break)); } uint16_t InteractionModelEngine::GetMinGuaranteedSubscriptionsPerFabric() const { #if CHIP_SYSTEM_CONFIG_POOL_USE_HEAP return UINT16_MAX; #else return static_cast( std::min(GetReadHandlerPoolCapacityForSubscriptions() / GetConfigMaxFabrics(), static_cast(UINT16_MAX))); #endif } size_t InteractionModelEngine::GetNumDirtySubscriptions() const { size_t numDirtySubscriptions = 0; mReadHandlers.ForEachActiveObject([&](const auto readHandler) { if (readHandler->IsType(ReadHandler::InteractionType::Subscribe) && readHandler->IsDirty()) { numDirtySubscriptions++; } return Loop::Continue; }); return numDirtySubscriptions; } void InteractionModelEngine::OnFabricRemoved(const FabricTable & fabricTable, FabricIndex fabricIndex) { mReadHandlers.ForEachActiveObject([fabricIndex](ReadHandler * handler) { if (handler->GetAccessingFabricIndex() == fabricIndex) { ChipLogProgress(InteractionModel, "Deleting expired ReadHandler for NodeId: " ChipLogFormatX64 ", FabricIndex: %u", ChipLogValueX64(handler->GetInitiatorNodeId()), fabricIndex); handler->Close(); } return Loop::Continue; }); #if CHIP_CONFIG_ENABLE_READ_CLIENT for (auto * readClient = mpActiveReadClientList; readClient != nullptr; readClient = readClient->GetNextClient()) { if (readClient->GetFabricIndex() == fabricIndex) { ChipLogProgress(InteractionModel, "Fabric removed, deleting obsolete read client with FabricIndex: %u", fabricIndex); readClient->Close(CHIP_ERROR_IM_FABRIC_DELETED, false); } } #endif // CHIP_CONFIG_ENABLE_READ_CLIENT for (auto & handler : mWriteHandlers) { if (!(handler.IsFree()) && handler.GetAccessingFabricIndex() == fabricIndex) { ChipLogProgress(InteractionModel, "Fabric removed, deleting obsolete write handler with FabricIndex: %u", fabricIndex); handler.Close(); } } // 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. } CHIP_ERROR InteractionModelEngine::ResumeSubscriptions() { #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS VerifyOrReturnError(mpSubscriptionResumptionStorage, CHIP_NO_ERROR); #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION VerifyOrReturnError(!mSubscriptionResumptionScheduled, CHIP_NO_ERROR); #endif // To avoid the case of a reboot loop causing rapid traffic generation / power consumption, subscription resumption should make // use of the persisted min-interval values, and wait before resumption. Ideally, each persisted subscription should wait their // own min-interval value before resumption, but that both A) potentially runs into a timer resource issue, and B) having a // low-powered device wake many times also has energy use implications. The logic below waits the largest of the persisted // min-interval values before resuming subscriptions. // Even though this causes subscription-to-subscription interaction by linking the min-interval values, this is the right thing // to do for now because it's both simple and avoids the timer resource and multiple-wake problems. This issue is to track // future improvements: https://github.com/project-chip/connectedhomeip/issues/25439 SubscriptionResumptionStorage::SubscriptionInfo subscriptionInfo; auto * iterator = mpSubscriptionResumptionStorage->IterateSubscriptions(); mNumOfSubscriptionsToResume = 0; uint16_t minInterval = 0; while (iterator->Next(subscriptionInfo)) { mNumOfSubscriptionsToResume++; minInterval = std::max(minInterval, subscriptionInfo.mMinInterval); } iterator->Release(); if (mNumOfSubscriptionsToResume) { #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION mSubscriptionResumptionScheduled = true; #endif ChipLogProgress(InteractionModel, "Resuming %d subscriptions in %u seconds", mNumOfSubscriptionsToResume, minInterval); ReturnErrorOnFailure(mpExchangeMgr->GetSessionManager()->SystemLayer()->StartTimer(System::Clock::Seconds16(minInterval), ResumeSubscriptionsTimerCallback, this)); } else { ChipLogProgress(InteractionModel, "No subscriptions to resume"); } #endif // CHIP_CONFIG_PERSIST_SUBSCRIPTIONS return CHIP_NO_ERROR; } void InteractionModelEngine::ResumeSubscriptionsTimerCallback(System::Layer * apSystemLayer, void * apAppState) { #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS VerifyOrReturn(apAppState != nullptr); InteractionModelEngine * imEngine = static_cast(apAppState); #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION imEngine->mSubscriptionResumptionScheduled = false; bool resumedSubscriptions = false; #endif // CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION SubscriptionResumptionStorage::SubscriptionInfo subscriptionInfo; AutoReleaseSubscriptionInfoIterator iterator(imEngine->mpSubscriptionResumptionStorage->IterateSubscriptions()); while (iterator->Next(subscriptionInfo)) { // If subscription happens between reboot and this timer callback, it's already live and should skip resumption if (Loop::Break == imEngine->mReadHandlers.ForEachActiveObject([&](ReadHandler * handler) { SubscriptionId subscriptionId; handler->GetSubscriptionId(subscriptionId); if (subscriptionId == subscriptionInfo.mSubscriptionId) { return Loop::Break; } return Loop::Continue; })) { ChipLogProgress(InteractionModel, "Skip resuming live subscriptionId %" PRIu32, subscriptionInfo.mSubscriptionId); continue; } auto subscriptionResumptionSessionEstablisher = Platform::MakeUnique(); if (subscriptionResumptionSessionEstablisher == nullptr) { ChipLogProgress(InteractionModel, "Failed to create SubscriptionResumptionSessionEstablisher"); return; } if (subscriptionResumptionSessionEstablisher->ResumeSubscription(*imEngine->mpCASESessionMgr, subscriptionInfo) != CHIP_NO_ERROR) { ChipLogProgress(InteractionModel, "Failed to ResumeSubscription 0x%" PRIx32, subscriptionInfo.mSubscriptionId); return; } subscriptionResumptionSessionEstablisher.release(); #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION resumedSubscriptions = true; #endif // CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION } #if CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION // If no persisted subscriptions needed resumption then all resumption retries are done if (!resumedSubscriptions) { imEngine->mNumSubscriptionResumptionRetries = 0; } #endif // CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION #endif // CHIP_CONFIG_PERSIST_SUBSCRIPTIONS } #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS && CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION uint32_t InteractionModelEngine::ComputeTimeSecondsTillNextSubscriptionResumption() { #if CONFIG_BUILD_FOR_HOST_UNIT_TEST if (mSubscriptionResumptionRetrySecondsOverride > 0) { return static_cast(mSubscriptionResumptionRetrySecondsOverride); } #endif if (mNumSubscriptionResumptionRetries > CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION_MAX_FIBONACCI_STEP_INDEX) { return CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION_MAX_RETRY_INTERVAL_SECS; } return CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION_MIN_RETRY_INTERVAL_SECS + GetFibonacciForIndex(mNumSubscriptionResumptionRetries) * CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION_WAIT_TIME_MULTIPLIER_SECS; } bool InteractionModelEngine::HasSubscriptionsToResume() { VerifyOrReturnValue(mpSubscriptionResumptionStorage != nullptr, false); // Look through persisted subscriptions and see if any aren't already in mReadHandlers pool SubscriptionResumptionStorage::SubscriptionInfo subscriptionInfo; auto * iterator = mpSubscriptionResumptionStorage->IterateSubscriptions(); bool foundSubscriptionToResume = false; while (iterator->Next(subscriptionInfo)) { if (Loop::Break == mReadHandlers.ForEachActiveObject([&](ReadHandler * handler) { SubscriptionId subscriptionId; handler->GetSubscriptionId(subscriptionId); if (subscriptionId == subscriptionInfo.mSubscriptionId) { return Loop::Break; } return Loop::Continue; })) { continue; } foundSubscriptionToResume = true; break; } iterator->Release(); return foundSubscriptionToResume; } #endif // CHIP_CONFIG_PERSIST_SUBSCRIPTIONS && CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION #if CHIP_CONFIG_PERSIST_SUBSCRIPTIONS void InteractionModelEngine::DecrementNumSubscriptionsToResume() { VerifyOrReturn(mNumOfSubscriptionsToResume > 0); #if CHIP_CONFIG_ENABLE_ICD_CIP && !CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION VerifyOrDie(mICDManager); #endif // CHIP_CONFIG_ENABLE_ICD_CIP && !CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION mNumOfSubscriptionsToResume--; #if CHIP_CONFIG_ENABLE_ICD_CIP && !CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION if (!mNumOfSubscriptionsToResume) { mICDManager->SetBootUpResumeSubscriptionExecuted(); } #endif // CHIP_CONFIG_ENABLE_ICD_CIP && !CHIP_CONFIG_SUBSCRIPTION_TIMEOUT_RESUMPTION } #endif // CHIP_CONFIG_PERSIST_SUBSCRIPTIONS } // namespace app } // namespace chip