| /* |
| * |
| * Copyright (c) 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 implements reporting engine for CHIP |
| * Data Model profile. |
| * |
| */ |
| |
| #include <app/icd/server/ICDServerConfig.h> |
| #if CHIP_CONFIG_ENABLE_ICD_SERVER |
| #include <app/icd/server/ICDNotifier.h> // nogncheck |
| #endif |
| #include <app/AppConfig.h> |
| #include <app/InteractionModelEngine.h> |
| #include <app/RequiredPrivilege.h> |
| #include <app/reporting/Engine.h> |
| #include <app/util/MatterCallbacks.h> |
| #include <app/util/ember-compatibility-functions.h> |
| |
| using namespace chip::Access; |
| |
| namespace chip { |
| namespace app { |
| namespace reporting { |
| |
| Engine::Engine(InteractionModelEngine * apImEngine) : mpImEngine(apImEngine) {} |
| |
| CHIP_ERROR Engine::Init() |
| { |
| mNumReportsInFlight = 0; |
| mCurReadHandlerIdx = 0; |
| return CHIP_NO_ERROR; |
| } |
| |
| void Engine::Shutdown() |
| { |
| // Flush out the event buffer synchronously |
| ScheduleUrgentEventDeliverySync(); |
| |
| mNumReportsInFlight = 0; |
| mCurReadHandlerIdx = 0; |
| mGlobalDirtySet.ReleaseAll(); |
| } |
| |
| bool Engine::IsClusterDataVersionMatch(const SingleLinkedListNode<DataVersionFilter> * aDataVersionFilterList, |
| const ConcreteReadAttributePath & aPath) |
| { |
| bool existPathMatch = false; |
| bool existVersionMismatch = false; |
| for (auto filter = aDataVersionFilterList; filter != nullptr; filter = filter->mpNext) |
| { |
| if (aPath.mEndpointId == filter->mValue.mEndpointId && aPath.mClusterId == filter->mValue.mClusterId) |
| { |
| existPathMatch = true; |
| if (!IsClusterDataVersionEqual(ConcreteClusterPath(filter->mValue.mEndpointId, filter->mValue.mClusterId), |
| filter->mValue.mDataVersion.Value())) |
| { |
| existVersionMismatch = true; |
| } |
| } |
| } |
| return existPathMatch && !existVersionMismatch; |
| } |
| |
| CHIP_ERROR |
| Engine::RetrieveClusterData(const SubjectDescriptor & aSubjectDescriptor, bool aIsFabricFiltered, |
| AttributeReportIBs::Builder & aAttributeReportIBs, const ConcreteReadAttributePath & aPath, |
| AttributeValueEncoder::AttributeEncodeState * aEncoderState) |
| { |
| ChipLogDetail(DataManagement, "<RE:Run> Cluster %" PRIx32 ", Attribute %" PRIx32 " is dirty", aPath.mClusterId, |
| aPath.mAttributeId); |
| |
| DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Read, |
| DataModelCallbacks::OperationOrder::Pre, aPath); |
| |
| ReturnErrorOnFailure(ReadSingleClusterData(aSubjectDescriptor, aIsFabricFiltered, aPath, aAttributeReportIBs, aEncoderState)); |
| |
| DataModelCallbacks::GetInstance()->AttributeOperation(DataModelCallbacks::OperationType::Read, |
| DataModelCallbacks::OperationOrder::Post, aPath); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| static bool IsOutOfWriterSpaceError(CHIP_ERROR err) |
| { |
| return err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL; |
| } |
| |
| CHIP_ERROR Engine::BuildSingleReportDataAttributeReportIBs(ReportDataMessage::Builder & aReportDataBuilder, |
| ReadHandler * apReadHandler, bool * apHasMoreChunks, |
| bool * apHasEncodedData) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| bool attributeDataWritten = false; |
| bool hasMoreChunks = true; |
| TLV::TLVWriter backup; |
| const uint32_t kReservedSizeEndOfReportIBs = 1; |
| bool reservedEndOfReportIBs = false; |
| |
| aReportDataBuilder.Checkpoint(backup); |
| |
| AttributeReportIBs::Builder & attributeReportIBs = aReportDataBuilder.CreateAttributeReportIBs(); |
| size_t emptyReportDataLength = 0; |
| |
| SuccessOrExit(err = aReportDataBuilder.GetError()); |
| |
| emptyReportDataLength = attributeReportIBs.GetWriter()->GetLengthWritten(); |
| // |
| // Reserve enough space for closing out the Report IB list |
| // |
| SuccessOrExit(err = attributeReportIBs.GetWriter()->ReserveBuffer(kReservedSizeEndOfReportIBs)); |
| reservedEndOfReportIBs = true; |
| |
| { |
| // TODO: Figure out how AttributePathExpandIterator should handle read |
| // vs write paths. |
| ConcreteAttributePath readPath; |
| |
| ChipLogDetail(DataManagement, |
| "Building Reports for ReadHandler with LastReportGeneration = 0x" ChipLogFormatX64 |
| " DirtyGeneration = 0x" ChipLogFormatX64, |
| ChipLogValueX64(apReadHandler->mPreviousReportsBeginGeneration), |
| ChipLogValueX64(apReadHandler->mDirtyGeneration)); |
| |
| // This ReadHandler is not generating reports, so we reset the iterator for a clean start. |
| if (!apReadHandler->IsReporting()) |
| { |
| apReadHandler->ResetPathIterator(); |
| } |
| |
| #if CONFIG_BUILD_FOR_HOST_UNIT_TEST |
| uint32_t attributesRead = 0; |
| #endif |
| |
| // For each path included in the interested path of the read handler... |
| for (; apReadHandler->GetAttributePathExpandIterator()->Get(readPath); |
| apReadHandler->GetAttributePathExpandIterator()->Next()) |
| { |
| if (!apReadHandler->IsPriming()) |
| { |
| bool concretePathDirty = false; |
| // TODO: Optimize this implementation by making the iterator only emit intersected paths. |
| mGlobalDirtySet.ForEachActiveObject([&](auto * dirtyPath) { |
| if (dirtyPath->IsAttributePathSupersetOf(readPath)) |
| { |
| // We don't need to worry about paths that were already marked dirty before the last time this read handler |
| // started a report that it completed: those paths already got reported. |
| if (dirtyPath->mGeneration > apReadHandler->mPreviousReportsBeginGeneration) |
| { |
| concretePathDirty = true; |
| return Loop::Break; |
| } |
| } |
| return Loop::Continue; |
| }); |
| |
| if (!concretePathDirty) |
| { |
| // This attribute is not dirty, we just skip this one. |
| continue; |
| } |
| } |
| else |
| { |
| if (IsClusterDataVersionMatch(apReadHandler->GetDataVersionFilterList(), readPath)) |
| { |
| continue; |
| } |
| } |
| |
| #if CONFIG_BUILD_FOR_HOST_UNIT_TEST |
| attributesRead++; |
| if (attributesRead > mMaxAttributesPerChunk) |
| { |
| ExitNow(err = CHIP_ERROR_BUFFER_TOO_SMALL); |
| } |
| #endif |
| |
| // If we are processing a read request, or the initial report of a subscription, just regard all paths as dirty |
| // paths. |
| TLV::TLVWriter attributeBackup; |
| attributeReportIBs.Checkpoint(attributeBackup); |
| ConcreteReadAttributePath pathForRetrieval(readPath); |
| // Load the saved state from previous encoding session for chunking of one single attribute (list chunking). |
| AttributeValueEncoder::AttributeEncodeState encodeState = apReadHandler->GetAttributeEncodeState(); |
| err = RetrieveClusterData(apReadHandler->GetSubjectDescriptor(), apReadHandler->IsFabricFiltered(), attributeReportIBs, |
| pathForRetrieval, &encodeState); |
| if (err != CHIP_NO_ERROR) |
| { |
| // If error is not an "out of writer space" error, rollback and encode status. |
| // Otherwise, if partial data allowed, save the encode state. |
| // Otherwise roll back. If we have already encoded some chunks, we are done; otherwise encode status. |
| |
| if (encodeState.AllowPartialData() && IsOutOfWriterSpaceError(err)) |
| { |
| ChipLogDetail(DataManagement, |
| "List does not fit in packet, chunk between list items for clusterId: " ChipLogFormatMEI |
| ", attributeId: " ChipLogFormatMEI, |
| ChipLogValueMEI(pathForRetrieval.mClusterId), ChipLogValueMEI(pathForRetrieval.mAttributeId)); |
| // Encoding is aborted but partial data is allowed, then we don't rollback and save the state for next chunk. |
| // The expectation is that RetrieveClusterData has already reset attributeReportIBs to a good state (rolled |
| // back any partially-written AttributeReportIB instances, reset its error status). Since AllowPartialData() |
| // is true, we may not have encoded a complete attribute value, but we did, if we encoded anything, encode a |
| // set of complete AttributeReportIB instances that represent part of the attribute value. |
| apReadHandler->SetAttributeEncodeState(encodeState); |
| } |
| else |
| { |
| // We met a error during writing reports, one common case is we are running out of buffer, rollback the |
| // attributeReportIB to avoid any partial data. |
| attributeReportIBs.Rollback(attributeBackup); |
| apReadHandler->SetAttributeEncodeState(AttributeValueEncoder::AttributeEncodeState()); |
| |
| if (!IsOutOfWriterSpaceError(err)) |
| { |
| ChipLogError(DataManagement, |
| "Fail to retrieve data, roll back and encode status on clusterId: " ChipLogFormatMEI |
| ", attributeId: " ChipLogFormatMEI "err = %" CHIP_ERROR_FORMAT, |
| ChipLogValueMEI(pathForRetrieval.mClusterId), ChipLogValueMEI(pathForRetrieval.mAttributeId), |
| err.Format()); |
| // Try to encode our error as a status response. |
| err = attributeReportIBs.EncodeAttributeStatus(pathForRetrieval, StatusIB(err)); |
| if (err != CHIP_NO_ERROR) |
| { |
| // OK, just roll back again and give up; if we still ran out of space we |
| // will send this status response in the next chunk. |
| attributeReportIBs.Rollback(attributeBackup); |
| } |
| } |
| else |
| { |
| ChipLogDetail(DataManagement, |
| "Next attribute value does not fit in packet, roll back on clusterId: " ChipLogFormatMEI |
| ", attributeId: " ChipLogFormatMEI ", err = %" CHIP_ERROR_FORMAT, |
| ChipLogValueMEI(pathForRetrieval.mClusterId), ChipLogValueMEI(pathForRetrieval.mAttributeId), |
| err.Format()); |
| } |
| } |
| } |
| SuccessOrExit(err); |
| // Successfully encoded the attribute, clear the internal state. |
| apReadHandler->SetAttributeEncodeState(AttributeValueEncoder::AttributeEncodeState()); |
| } |
| // We just visited all paths interested by this read handler and did not abort in the middle of iteration, there are no more |
| // chunks for this report. |
| hasMoreChunks = false; |
| } |
| exit: |
| if (attributeReportIBs.GetWriter()->GetLengthWritten() != emptyReportDataLength) |
| { |
| // We may encounter BUFFER_TOO_SMALL with nothing actually written for the case of list chunking, so we check if we have |
| // actually |
| attributeDataWritten = true; |
| } |
| |
| if (apHasEncodedData != nullptr) |
| { |
| *apHasEncodedData = attributeDataWritten; |
| } |
| // |
| // Running out of space is an error that we're expected to handle - the incompletely written DataIB has already been rolled back |
| // earlier to ensure only whole and complete DataIBs are present in the stream. |
| // |
| // We can safely clear out the error so that the rest of the machinery to close out the reports, etc. will function correctly. |
| // These are are guaranteed to not fail since we've already reserved memory for the remaining 'close out' TLV operations in this |
| // function and its callers. |
| // |
| if (IsOutOfWriterSpaceError(err) && reservedEndOfReportIBs) |
| { |
| ChipLogDetail(DataManagement, "<RE:Run> We cannot put more chunks into this report. Enable chunking."); |
| err = CHIP_NO_ERROR; |
| } |
| |
| // |
| // Only close out the report if we haven't hit an error yet so far. |
| // |
| if (err == CHIP_NO_ERROR) |
| { |
| attributeReportIBs.GetWriter()->UnreserveBuffer(kReservedSizeEndOfReportIBs); |
| |
| err = attributeReportIBs.EndOfAttributeReportIBs(); |
| |
| // |
| // We reserved space for this earlier - consequently, the call to end the ReportIBs should |
| // never fail, so assert if we do since that's a logic bug. |
| // |
| VerifyOrDie(err == CHIP_NO_ERROR); |
| } |
| |
| // |
| // Rollback the the entire ReportIB array if we never wrote any attributes |
| // AND never hit an error. |
| // |
| if (!attributeDataWritten && err == CHIP_NO_ERROR) |
| { |
| aReportDataBuilder.Rollback(backup); |
| } |
| |
| // hasMoreChunks + no data encoded is a flag that we have encountered some trouble when processing the attribute. |
| // BuildAndSendSingleReportData will abort the read transaction if we encoded no attribute and no events but hasMoreChunks is |
| // set. |
| if (apHasMoreChunks != nullptr) |
| { |
| *apHasMoreChunks = hasMoreChunks; |
| } |
| |
| return err; |
| } |
| |
| CHIP_ERROR Engine::CheckAccessDeniedEventPaths(TLV::TLVWriter & aWriter, bool & aHasEncodedData, ReadHandler * apReadHandler) |
| { |
| using Protocols::InteractionModel::Status; |
| |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| for (auto current = apReadHandler->mpEventPathList; current != nullptr;) |
| { |
| if (current->mValue.IsWildcardPath()) |
| { |
| current = current->mpNext; |
| continue; |
| } |
| |
| ConcreteEventPath path(current->mValue.mEndpointId, current->mValue.mClusterId, current->mValue.mEventId); |
| Status status = CheckEventSupportStatus(path); |
| if (status != Status::Success) |
| { |
| TLV::TLVWriter checkpoint = aWriter; |
| err = EventReportIB::ConstructEventStatusIB(aWriter, path, StatusIB(status)); |
| if (err != CHIP_NO_ERROR) |
| { |
| aWriter = checkpoint; |
| break; |
| } |
| aHasEncodedData = true; |
| } |
| |
| Access::RequestPath requestPath{ .cluster = current->mValue.mClusterId, .endpoint = current->mValue.mEndpointId }; |
| Access::Privilege requestPrivilege = RequiredPrivilege::ForReadEvent(path); |
| |
| err = Access::GetAccessControl().Check(apReadHandler->GetSubjectDescriptor(), requestPath, requestPrivilege); |
| if (err != CHIP_ERROR_ACCESS_DENIED) |
| { |
| ReturnErrorOnFailure(err); |
| } |
| else |
| { |
| TLV::TLVWriter checkpoint = aWriter; |
| err = EventReportIB::ConstructEventStatusIB(aWriter, path, StatusIB(Status::UnsupportedAccess)); |
| if (err != CHIP_NO_ERROR) |
| { |
| aWriter = checkpoint; |
| break; |
| } |
| aHasEncodedData = true; |
| ChipLogDetail(InteractionModel, "Access to event (%u, " ChipLogFormatMEI ", " ChipLogFormatMEI ") denied by ACL", |
| current->mValue.mEndpointId, ChipLogValueMEI(current->mValue.mClusterId), |
| ChipLogValueMEI(current->mValue.mEventId)); |
| } |
| current = current->mpNext; |
| } |
| |
| return err; |
| } |
| |
| CHIP_ERROR Engine::BuildSingleReportDataEventReports(ReportDataMessage::Builder & aReportDataBuilder, ReadHandler * apReadHandler, |
| bool aBufferIsUsed, bool * apHasMoreChunks, bool * apHasEncodedData) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| size_t eventCount = 0; |
| bool hasEncodedStatus = false; |
| TLV::TLVWriter backup; |
| bool eventClean = true; |
| auto & eventMin = apReadHandler->GetEventMin(); |
| EventManagement & eventManager = EventManagement::GetInstance(); |
| bool hasMoreChunks = false; |
| |
| aReportDataBuilder.Checkpoint(backup); |
| |
| VerifyOrExit(apReadHandler->GetEventPathList() != nullptr, ); |
| |
| // If the eventManager is not valid or has not been initialized, |
| // skip the rest of processing |
| VerifyOrExit(eventManager.IsValid(), ChipLogError(DataManagement, "EventManagement has not yet initialized")); |
| |
| eventClean = apReadHandler->CheckEventClean(eventManager); |
| |
| // proceed only if there are new events. |
| if (eventClean) |
| { |
| ExitNow(); // Read clean, move along |
| } |
| |
| { |
| // Just like what we do in BuildSingleReportDataAttributeReportIBs(), we need to reserve one byte for end of container tag |
| // when encoding events to ensure we can close the container successfully. |
| const uint32_t kReservedSizeEndOfReportIBs = 1; |
| EventReportIBs::Builder & eventReportIBs = aReportDataBuilder.CreateEventReports(); |
| SuccessOrExit(err = aReportDataBuilder.GetError()); |
| VerifyOrExit(eventReportIBs.GetWriter() != nullptr, err = CHIP_ERROR_INCORRECT_STATE); |
| SuccessOrExit(err = eventReportIBs.GetWriter()->ReserveBuffer(kReservedSizeEndOfReportIBs)); |
| |
| err = CheckAccessDeniedEventPaths(*(eventReportIBs.GetWriter()), hasEncodedStatus, apReadHandler); |
| SuccessOrExit(err); |
| |
| err = eventManager.FetchEventsSince(*(eventReportIBs.GetWriter()), apReadHandler->GetEventPathList(), eventMin, eventCount, |
| apReadHandler->GetSubjectDescriptor()); |
| |
| if ((err == CHIP_END_OF_TLV) || (err == CHIP_ERROR_TLV_UNDERRUN) || (err == CHIP_NO_ERROR)) |
| { |
| err = CHIP_NO_ERROR; |
| hasMoreChunks = false; |
| } |
| else if (IsOutOfWriterSpaceError(err)) |
| { |
| // when first cluster event is too big to fit in the packet, ignore that cluster event. |
| // However, we may have encoded some attributes before, we don't skip it in that case. |
| if (eventCount == 0) |
| { |
| if (!aBufferIsUsed) |
| { |
| eventMin++; |
| } |
| ChipLogDetail(DataManagement, "<RE:Run> first cluster event is too big so that it fails to fit in the packet!"); |
| err = CHIP_NO_ERROR; |
| } |
| else |
| { |
| // `FetchEventsSince` has filled the available space |
| // within the allowed buffer before it fit all the |
| // available events. This is an expected condition, |
| // so we do not propagate the error to higher levels; |
| // instead, we terminate the event processing for now |
| err = CHIP_NO_ERROR; |
| } |
| hasMoreChunks = true; |
| } |
| else |
| { |
| // All other errors are propagated to higher level. |
| // Exiting here and returning an error will lead to |
| // abandoning subscription. |
| ExitNow(); |
| } |
| |
| SuccessOrExit(err = eventReportIBs.GetWriter()->UnreserveBuffer(kReservedSizeEndOfReportIBs)); |
| SuccessOrExit(err = eventReportIBs.EndOfEventReports()); |
| } |
| ChipLogDetail(DataManagement, "Fetched %u events", static_cast<unsigned int>(eventCount)); |
| |
| exit: |
| if (apHasEncodedData != nullptr) |
| { |
| *apHasEncodedData = hasEncodedStatus || (eventCount != 0); |
| } |
| |
| // Maybe encoding the attributes has already used up all space. |
| if ((err == CHIP_NO_ERROR || IsOutOfWriterSpaceError(err)) && !(hasEncodedStatus || (eventCount != 0))) |
| { |
| aReportDataBuilder.Rollback(backup); |
| err = CHIP_NO_ERROR; |
| } |
| |
| // hasMoreChunks + no data encoded is a flag that we have encountered some trouble when processing the attribute. |
| // BuildAndSendSingleReportData will abort the read transaction if we encoded no attribute and no events but hasMoreChunks is |
| // set. |
| if (apHasMoreChunks != nullptr) |
| { |
| *apHasMoreChunks = hasMoreChunks; |
| } |
| return err; |
| } |
| |
| CHIP_ERROR Engine::BuildAndSendSingleReportData(ReadHandler * apReadHandler) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| chip::System::PacketBufferTLVWriter reportDataWriter; |
| ReportDataMessage::Builder reportDataBuilder; |
| chip::System::PacketBufferHandle bufHandle = System::PacketBufferHandle::New(chip::app::kMaxSecureSduLengthBytes); |
| uint16_t reservedSize = 0; |
| bool hasMoreChunks = false; |
| bool needCloseReadHandler = false; |
| |
| // Reserved size for the MoreChunks boolean flag, which takes up 1 byte for the control tag and 1 byte for the context tag. |
| const uint32_t kReservedSizeForMoreChunksFlag = 1 + 1; |
| |
| // Reserved size for the uint8_t InteractionModelRevision flag, which takes up 1 byte for the control tag and 1 byte for the |
| // context tag, 1 byte for value |
| const uint32_t kReservedSizeForIMRevision = 1 + 1 + 1; |
| |
| // Reserved size for the end of report message, which is an end-of-container (i.e 1 byte for the control tag). |
| const uint32_t kReservedSizeForEndOfReportMessage = 1; |
| |
| // Reserved size for an empty EventReportIBs, so we can at least check if there are any events need to be reported. |
| const uint32_t kReservedSizeForEventReportIBs = 3; // type, tag, end of container |
| |
| VerifyOrExit(apReadHandler != nullptr, err = CHIP_ERROR_INVALID_ARGUMENT); |
| VerifyOrExit(apReadHandler->GetSession() != nullptr, err = CHIP_ERROR_INCORRECT_STATE); |
| VerifyOrExit(!bufHandle.IsNull(), err = CHIP_ERROR_NO_MEMORY); |
| |
| if (bufHandle->AvailableDataLength() > kMaxSecureSduLengthBytes) |
| { |
| reservedSize = static_cast<uint16_t>(bufHandle->AvailableDataLength() - kMaxSecureSduLengthBytes); |
| } |
| |
| reportDataWriter.Init(std::move(bufHandle)); |
| |
| #if CONFIG_BUILD_FOR_HOST_UNIT_TEST |
| reportDataWriter.ReserveBuffer(mReservedSize); |
| #endif |
| |
| // Always limit the size of the generated packet to fit within kMaxSecureSduLengthBytes regardless of the available buffer |
| // capacity. |
| // Also, we need to reserve some extra space for the MIC field. |
| reportDataWriter.ReserveBuffer(static_cast<uint32_t>(reservedSize + chip::Crypto::CHIP_CRYPTO_AEAD_MIC_LENGTH_BYTES)); |
| |
| // Create a report data. |
| err = reportDataBuilder.Init(&reportDataWriter); |
| SuccessOrExit(err); |
| |
| if (apReadHandler->IsType(ReadHandler::InteractionType::Subscribe)) |
| { |
| #if CHIP_CONFIG_ENABLE_ICD_SERVER |
| // Notify the ICDManager that we are about to send a subscription report before we prepare the Report payload. |
| // This allows the ICDManager to trigger any necessary updates and have the information in the report about to be sent. |
| app::ICDNotifier::GetInstance().NotifySubscriptionReport(); |
| #endif // CHIP_CONFIG_ENABLE_ICD_SERVER |
| |
| SubscriptionId subscriptionId = 0; |
| apReadHandler->GetSubscriptionId(subscriptionId); |
| reportDataBuilder.SubscriptionId(subscriptionId); |
| } |
| |
| SuccessOrExit(err = reportDataWriter.ReserveBuffer(kReservedSizeForMoreChunksFlag + kReservedSizeForIMRevision + |
| kReservedSizeForEndOfReportMessage + kReservedSizeForEventReportIBs)); |
| |
| { |
| bool hasMoreChunksForAttributes = false; |
| bool hasMoreChunksForEvents = false; |
| bool hasEncodedAttributes = false; |
| bool hasEncodedEvents = false; |
| |
| err = BuildSingleReportDataAttributeReportIBs(reportDataBuilder, apReadHandler, &hasMoreChunksForAttributes, |
| &hasEncodedAttributes); |
| SuccessOrExit(err); |
| SuccessOrExit(err = reportDataWriter.UnreserveBuffer(kReservedSizeForEventReportIBs)); |
| err = BuildSingleReportDataEventReports(reportDataBuilder, apReadHandler, hasEncodedAttributes, &hasMoreChunksForEvents, |
| &hasEncodedEvents); |
| SuccessOrExit(err); |
| |
| hasMoreChunks = hasMoreChunksForAttributes || hasMoreChunksForEvents; |
| |
| if (!hasEncodedAttributes && !hasEncodedEvents && hasMoreChunks) |
| { |
| ChipLogError(DataManagement, |
| "No data actually encoded but hasMoreChunks flag is set, close read handler! (attribute too big?)"); |
| err = apReadHandler->SendStatusReport(Protocols::InteractionModel::Status::ResourceExhausted); |
| if (err == CHIP_NO_ERROR) |
| { |
| needCloseReadHandler = true; |
| } |
| ExitNow(); |
| } |
| } |
| |
| SuccessOrExit(err = reportDataBuilder.GetError()); |
| SuccessOrExit(err = reportDataWriter.UnreserveBuffer(kReservedSizeForMoreChunksFlag + kReservedSizeForIMRevision + |
| kReservedSizeForEndOfReportMessage)); |
| if (hasMoreChunks) |
| { |
| reportDataBuilder.MoreChunkedMessages(true); |
| } |
| else if (apReadHandler->IsType(ReadHandler::InteractionType::Read)) |
| { |
| reportDataBuilder.SuppressResponse(true); |
| } |
| |
| reportDataBuilder.EndOfReportDataMessage(); |
| |
| // |
| // Since we've already reserved space for both the MoreChunked/SuppressResponse flags, as well as |
| // the end-of-container flag for the end of the report, we should never hit an error closing out the message. |
| // |
| VerifyOrDie(reportDataBuilder.GetError() == CHIP_NO_ERROR); |
| |
| err = reportDataWriter.Finalize(&bufHandle); |
| SuccessOrExit(err); |
| |
| ChipLogDetail(DataManagement, "<RE> Sending report (payload has %" PRIu32 " bytes)...", reportDataWriter.GetLengthWritten()); |
| err = SendReport(apReadHandler, std::move(bufHandle), hasMoreChunks); |
| VerifyOrExit(err == CHIP_NO_ERROR, |
| ChipLogError(DataManagement, "<RE> Error sending out report data with %" CHIP_ERROR_FORMAT "!", err.Format())); |
| |
| ChipLogDetail(DataManagement, "<RE> ReportsInFlight = %" PRIu32 " with readHandler %" PRIu32 ", RE has %s", mNumReportsInFlight, |
| mCurReadHandlerIdx, hasMoreChunks ? "more messages" : "no more messages"); |
| |
| exit: |
| if (err != CHIP_NO_ERROR || (apReadHandler->IsType(ReadHandler::InteractionType::Read) && !hasMoreChunks) || |
| needCloseReadHandler) |
| { |
| // |
| // In the case of successful report generation and we're on the last chunk of a read, we don't expect |
| // any further activity on this exchange. The EC layer will automatically close our EC, so shutdown the ReadHandler |
| // gracefully. |
| // |
| apReadHandler->Close(); |
| } |
| |
| return err; |
| } |
| |
| void Engine::Run(System::Layer * aSystemLayer, void * apAppState) |
| { |
| Engine * const pEngine = reinterpret_cast<Engine *>(apAppState); |
| pEngine->mRunScheduled = false; |
| pEngine->Run(); |
| } |
| |
| CHIP_ERROR Engine::ScheduleRun() |
| { |
| if (IsRunScheduled()) |
| { |
| return CHIP_NO_ERROR; |
| } |
| |
| Messaging::ExchangeManager * exchangeManager = mpImEngine->GetExchangeManager(); |
| if (exchangeManager == nullptr) |
| { |
| return CHIP_ERROR_INCORRECT_STATE; |
| } |
| SessionManager * sessionManager = exchangeManager->GetSessionManager(); |
| if (sessionManager == nullptr) |
| { |
| return CHIP_ERROR_INCORRECT_STATE; |
| } |
| System::Layer * systemLayer = sessionManager->SystemLayer(); |
| if (systemLayer == nullptr) |
| { |
| return CHIP_ERROR_INCORRECT_STATE; |
| } |
| ReturnErrorOnFailure(systemLayer->ScheduleWork(Run, this)); |
| mRunScheduled = true; |
| return CHIP_NO_ERROR; |
| } |
| |
| void Engine::Run() |
| { |
| uint32_t numReadHandled = 0; |
| |
| // We may be deallocating read handlers as we go. Track how many we had |
| // initially, so we make sure to go through all of them. |
| size_t initialAllocated = mpImEngine->mReadHandlers.Allocated(); |
| while ((mNumReportsInFlight < CHIP_IM_MAX_REPORTS_IN_FLIGHT) && (numReadHandled < initialAllocated)) |
| { |
| ReadHandler * readHandler = |
| mpImEngine->ActiveHandlerAt(mCurReadHandlerIdx % (uint32_t) mpImEngine->mReadHandlers.Allocated()); |
| VerifyOrDie(readHandler != nullptr); |
| |
| if (readHandler->ShouldReportUnscheduled() || mpImEngine->GetReportScheduler()->IsReportableNow(readHandler)) |
| { |
| |
| mRunningReadHandler = readHandler; |
| CHIP_ERROR err = BuildAndSendSingleReportData(readHandler); |
| mRunningReadHandler = nullptr; |
| if (err != CHIP_NO_ERROR) |
| { |
| return; |
| } |
| } |
| |
| numReadHandled++; |
| // If readHandler removed itself from our list, we also decremented |
| // mCurReadHandlerIdx to account for that removal, so it's safe to |
| // increment here. |
| mCurReadHandlerIdx++; |
| } |
| |
| // |
| // If our tracker has exceeded the bounds of the handler list, reset it back to 0. |
| // This isn't strictly necessary, but does make it easier to debug issues in this code if they |
| // do arise. |
| // |
| if (mCurReadHandlerIdx >= mpImEngine->mReadHandlers.Allocated()) |
| { |
| mCurReadHandlerIdx = 0; |
| } |
| |
| bool allReadClean = true; |
| |
| mpImEngine->mReadHandlers.ForEachActiveObject([&allReadClean](ReadHandler * handler) { |
| if (handler->IsDirty()) |
| { |
| allReadClean = false; |
| return Loop::Break; |
| } |
| |
| return Loop::Continue; |
| }); |
| |
| if (allReadClean) |
| { |
| ChipLogDetail(DataManagement, "All ReadHandler-s are clean, clear GlobalDirtySet"); |
| |
| mGlobalDirtySet.ReleaseAll(); |
| } |
| } |
| |
| bool Engine::MergeOverlappedAttributePath(const AttributePathParams & aAttributePath) |
| { |
| return Loop::Break == mGlobalDirtySet.ForEachActiveObject([&](auto * path) { |
| if (path->IsAttributePathSupersetOf(aAttributePath)) |
| { |
| path->mGeneration = GetDirtySetGeneration(); |
| return Loop::Break; |
| } |
| if (aAttributePath.IsAttributePathSupersetOf(*path)) |
| { |
| // TODO: the wildcard input path may be superset of next paths in globalDirtySet, it is fine at this moment, since |
| // when building report, it would use the first path of globalDirtySet to compare against interested paths read clients |
| // want. |
| // It is better to eliminate the duplicate wildcard paths in follow-up |
| path->mGeneration = GetDirtySetGeneration(); |
| path->mEndpointId = aAttributePath.mEndpointId; |
| path->mClusterId = aAttributePath.mClusterId; |
| path->mListIndex = aAttributePath.mListIndex; |
| path->mAttributeId = aAttributePath.mAttributeId; |
| return Loop::Break; |
| } |
| return Loop::Continue; |
| }); |
| } |
| |
| bool Engine::ClearTombPaths() |
| { |
| bool pathReleased = false; |
| mGlobalDirtySet.ForEachActiveObject([&](auto * path) { |
| if (path->mGeneration == 0) |
| { |
| mGlobalDirtySet.ReleaseObject(path); |
| pathReleased = true; |
| } |
| return Loop::Continue; |
| }); |
| return pathReleased; |
| } |
| |
| bool Engine::MergeDirtyPathsUnderSameCluster() |
| { |
| mGlobalDirtySet.ForEachActiveObject([&](auto * outerPath) { |
| if (outerPath->HasWildcardClusterId() || outerPath->mGeneration == 0) |
| { |
| return Loop::Continue; |
| } |
| mGlobalDirtySet.ForEachActiveObject([&](auto * innerPath) { |
| if (innerPath == outerPath) |
| { |
| return Loop::Continue; |
| } |
| // We don't support paths with a wildcard endpoint + a concrete cluster in global dirty set, so we do a simple == check |
| // here. |
| if (innerPath->mEndpointId != outerPath->mEndpointId || innerPath->mClusterId != outerPath->mClusterId) |
| { |
| return Loop::Continue; |
| } |
| if (innerPath->mGeneration > outerPath->mGeneration) |
| { |
| outerPath->mGeneration = innerPath->mGeneration; |
| } |
| outerPath->SetWildcardAttributeId(); |
| |
| // The object pool does not allow us to release objects in a nested iteration, mark the path as a tomb by setting its |
| // generation to 0 and then clear it later. |
| innerPath->mGeneration = 0; |
| return Loop::Continue; |
| }); |
| return Loop::Continue; |
| }); |
| |
| return ClearTombPaths(); |
| } |
| |
| bool Engine::MergeDirtyPathsUnderSameEndpoint() |
| { |
| mGlobalDirtySet.ForEachActiveObject([&](auto * outerPath) { |
| if (outerPath->HasWildcardEndpointId() || outerPath->mGeneration == 0) |
| { |
| return Loop::Continue; |
| } |
| mGlobalDirtySet.ForEachActiveObject([&](auto * innerPath) { |
| if (innerPath == outerPath) |
| { |
| return Loop::Continue; |
| } |
| if (innerPath->mEndpointId != outerPath->mEndpointId) |
| { |
| return Loop::Continue; |
| } |
| if (innerPath->mGeneration > outerPath->mGeneration) |
| { |
| outerPath->mGeneration = innerPath->mGeneration; |
| } |
| outerPath->SetWildcardClusterId(); |
| outerPath->SetWildcardAttributeId(); |
| |
| // The object pool does not allow us to release objects in a nested iteration, mark the path as a tomb by setting its |
| // generation to 0 and then clear it later. |
| innerPath->mGeneration = 0; |
| return Loop::Continue; |
| }); |
| return Loop::Continue; |
| }); |
| return ClearTombPaths(); |
| } |
| |
| CHIP_ERROR Engine::InsertPathIntoDirtySet(const AttributePathParams & aAttributePath) |
| { |
| ReturnErrorCodeIf(MergeOverlappedAttributePath(aAttributePath), CHIP_NO_ERROR); |
| |
| if (mGlobalDirtySet.Exhausted() && !MergeDirtyPathsUnderSameCluster() && !MergeDirtyPathsUnderSameEndpoint()) |
| { |
| ChipLogDetail(DataManagement, "Global dirty set pool exhausted, merge all paths."); |
| mGlobalDirtySet.ReleaseAll(); |
| auto object = mGlobalDirtySet.CreateObject(); |
| object->mGeneration = GetDirtySetGeneration(); |
| } |
| |
| ReturnErrorCodeIf(MergeOverlappedAttributePath(aAttributePath), CHIP_NO_ERROR); |
| ChipLogDetail(DataManagement, "Cannot merge the new path into any existing path, create one."); |
| |
| auto object = mGlobalDirtySet.CreateObject(); |
| if (object == nullptr) |
| { |
| // This should not happen, this path should be merged into the wildcard endpoint at least. |
| ChipLogError(DataManagement, "mGlobalDirtySet pool full, cannot handle more entries!"); |
| return CHIP_ERROR_NO_MEMORY; |
| } |
| *object = aAttributePath; |
| object->mGeneration = GetDirtySetGeneration(); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR Engine::SetDirty(AttributePathParams & aAttributePath) |
| { |
| BumpDirtySetGeneration(); |
| |
| bool intersectsInterestPath = false; |
| mpImEngine->mReadHandlers.ForEachActiveObject([&aAttributePath, &intersectsInterestPath](ReadHandler * handler) { |
| // We call AttributePathIsDirty for both read interactions and subscribe interactions, since we may send inconsistent |
| // attribute data between two chunks. AttributePathIsDirty will not schedule a new run for read handlers which are |
| // waiting for a response to the last message chunk for read interactions. |
| if (handler->CanStartReporting() || handler->IsAwaitingReportResponse()) |
| { |
| for (auto object = handler->GetAttributePathList(); object != nullptr; object = object->mpNext) |
| { |
| if (object->mValue.Intersects(aAttributePath)) |
| { |
| handler->AttributePathIsDirty(aAttributePath); |
| intersectsInterestPath = true; |
| break; |
| } |
| } |
| } |
| |
| return Loop::Continue; |
| }); |
| |
| if (!intersectsInterestPath) |
| { |
| return CHIP_NO_ERROR; |
| } |
| ReturnErrorOnFailure(InsertPathIntoDirtySet(aAttributePath)); |
| |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR Engine::SendReport(ReadHandler * apReadHandler, System::PacketBufferHandle && aPayload, bool aHasMoreChunks) |
| { |
| CHIP_ERROR err = CHIP_NO_ERROR; |
| |
| // We can only have 1 report in flight for any given read - increment and break out. |
| mNumReportsInFlight++; |
| err = apReadHandler->SendReportData(std::move(aPayload), aHasMoreChunks); |
| if (err != CHIP_NO_ERROR) |
| { |
| --mNumReportsInFlight; |
| } |
| return err; |
| } |
| |
| void Engine::OnReportConfirm() |
| { |
| VerifyOrDie(mNumReportsInFlight > 0); |
| |
| if (mNumReportsInFlight == CHIP_IM_MAX_REPORTS_IN_FLIGHT) |
| { |
| // We could have other things waiting to go now that this report is no |
| // longer in flight. |
| ScheduleRun(); |
| } |
| mNumReportsInFlight--; |
| ChipLogDetail(DataManagement, "<RE> OnReportConfirm: NumReports = %" PRIu32, mNumReportsInFlight); |
| } |
| |
| void Engine::GetMinEventLogPosition(uint32_t & aMinLogPosition) |
| { |
| mpImEngine->mReadHandlers.ForEachActiveObject([&aMinLogPosition](ReadHandler * handler) { |
| if (handler->IsType(ReadHandler::InteractionType::Read)) |
| { |
| return Loop::Continue; |
| } |
| |
| uint32_t initialWrittenEventsBytes = handler->GetLastWrittenEventsBytes(); |
| if (initialWrittenEventsBytes < aMinLogPosition) |
| { |
| aMinLogPosition = initialWrittenEventsBytes; |
| } |
| |
| return Loop::Continue; |
| }); |
| } |
| |
| CHIP_ERROR Engine::ScheduleBufferPressureEventDelivery(uint32_t aBytesWritten) |
| { |
| uint32_t minEventLogPosition = aBytesWritten; |
| GetMinEventLogPosition(minEventLogPosition); |
| if (aBytesWritten - minEventLogPosition > CHIP_CONFIG_EVENT_LOGGING_BYTE_THRESHOLD) |
| { |
| ChipLogDetail(DataManagement, "<RE> Buffer overfilled CHIP_CONFIG_EVENT_LOGGING_BYTE_THRESHOLD %d, schedule engine run", |
| CHIP_CONFIG_EVENT_LOGGING_BYTE_THRESHOLD); |
| return ScheduleRun(); |
| } |
| return CHIP_NO_ERROR; |
| } |
| |
| CHIP_ERROR Engine::ScheduleEventDelivery(ConcreteEventPath & aPath, uint32_t aBytesWritten) |
| { |
| // If we literally have no read handlers right now that care about any events, |
| // we don't need to call schedule run for event. |
| // If schedule run is called, actually we would not delivery events as well. |
| // Just wanna save one schedule run here |
| if (mpImEngine->mEventPathPool.Allocated() == 0) |
| { |
| return CHIP_NO_ERROR; |
| } |
| |
| bool isUrgentEvent = false; |
| mpImEngine->mReadHandlers.ForEachActiveObject([&aPath, &isUrgentEvent](ReadHandler * handler) { |
| if (handler->IsType(ReadHandler::InteractionType::Read)) |
| { |
| return Loop::Continue; |
| } |
| |
| for (auto * interestedPath = handler->GetEventPathList(); interestedPath != nullptr; |
| interestedPath = interestedPath->mpNext) |
| { |
| if (interestedPath->mValue.IsEventPathSupersetOf(aPath) && interestedPath->mValue.mIsUrgentEvent) |
| { |
| isUrgentEvent = true; |
| handler->ForceDirtyState(); |
| break; |
| } |
| } |
| |
| return Loop::Continue; |
| }); |
| |
| if (isUrgentEvent) |
| { |
| ChipLogDetail(DataManagement, "Urgent event will be sent once reporting is not blocked by the min interval"); |
| return CHIP_NO_ERROR; |
| } |
| |
| return ScheduleBufferPressureEventDelivery(aBytesWritten); |
| } |
| |
| void Engine::ScheduleUrgentEventDeliverySync(Optional<FabricIndex> fabricIndex) |
| { |
| mpImEngine->mReadHandlers.ForEachActiveObject([fabricIndex](ReadHandler * handler) { |
| if (handler->IsType(ReadHandler::InteractionType::Read)) |
| { |
| return Loop::Continue; |
| } |
| |
| if (fabricIndex.HasValue() && fabricIndex.Value() != handler->GetAccessingFabricIndex()) |
| { |
| return Loop::Continue; |
| } |
| |
| handler->ForceDirtyState(); |
| |
| return Loop::Continue; |
| }); |
| |
| Run(); |
| } |
| |
| }; // namespace reporting |
| } // namespace app |
| } // namespace chip |
| |
| // TODO: MatterReportingAttributeChangeCallback should just live in libCHIP, |
| // instead of being in ember-compatibility-functions. It does not depend on any |
| // app-specific generated bits. |
| void __attribute__((weak)) |
| MatterReportingAttributeChangeCallback(chip::EndpointId endpoint, chip::ClusterId clusterId, chip::AttributeId attributeId) |
| {} |