Improve description for MTRDevice. (#34857)
* apply changes from PR #34797 to to concrete class
* Improve description for MTRDevice.
Includes various information that wasn't included before but probably should be.
* Address review comments.
---------
Co-authored-by: Kiel Oleson <kielo@apple.com>
diff --git a/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.h b/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.h
index 598bbf0..f147307 100644
--- a/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.h
+++ b/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.h
@@ -228,6 +228,9 @@
- (BOOL)hasDuplicateForTypeID:(NSUInteger)opaqueDuplicateTypeID
workItemData:(id)opaqueWorkItemData;
+// Returns current count of queued items.
+- (NSUInteger)itemCount;
+
/// Cancels and removes all work items.
- (void)invalidate;
@end
diff --git a/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.mm b/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.mm
index 4d82fde..de3faee 100644
--- a/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.mm
+++ b/src/darwin/Framework/CHIP/MTRAsyncWorkQueue.mm
@@ -433,6 +433,12 @@
return NO;
}
+- (NSUInteger)itemCount
+{
+ std::lock_guard lock(_lock);
+ return _items.count;
+}
+
@end
NS_ASSUME_NONNULL_END
diff --git a/src/darwin/Framework/CHIP/MTRDevice.mm b/src/darwin/Framework/CHIP/MTRDevice.mm
index 9ce2e3d..d8fc52b 100644
--- a/src/darwin/Framework/CHIP/MTRDevice.mm
+++ b/src/darwin/Framework/CHIP/MTRDevice.mm
@@ -209,6 +209,24 @@
{
return state == MTRInternalDeviceStateInitialSubscriptionEstablished || state == MTRInternalDeviceStateLaterSubscriptionEstablished;
}
+
+NSString * InternalDeviceStateString(MTRInternalDeviceState state)
+{
+ switch (state) {
+ case MTRInternalDeviceStateUnsubscribed:
+ return @"Unsubscribed";
+ case MTRInternalDeviceStateSubscribing:
+ return @"Subscribing";
+ case MTRInternalDeviceStateInitialSubscriptionEstablished:
+ return @"InitialSubscriptionEstablished";
+ case MTRInternalDeviceStateResubscribing:
+ return @"Resubscribing";
+ case MTRInternalDeviceStateLaterSubscriptionEstablished:
+ return @"LaterSubscriptionEstablished";
+ default:
+ return @"Unknown";
+ }
+}
} // anonymous namespace
typedef NS_ENUM(NSUInteger, MTRDeviceExpectedValueFieldIndex) {
@@ -355,8 +373,11 @@
@interface MTRDevice ()
@property (nonatomic, readonly) os_unfair_lock lock; // protects the caches and device state
// protects against concurrent time updates by guarding timeUpdateScheduled flag which manages time updates scheduling,
-// and protects device calls to setUTCTime and setDSTOffset
+// and protects device calls to setUTCTime and setDSTOffset. This can't just be replaced with "lock", because the time
+// update code calls public APIs like readAttributeWithEndpointID:.. (which attempt to take "lock") while holding
+// whatever lock protects the time sync bits.
@property (nonatomic, readonly) os_unfair_lock timeSyncLock;
+
@property (nonatomic) chip::FabricIndex fabricIndex;
@property (nonatomic) NSMutableArray<NSDictionary<NSString *, id> *> * unreportedEvents;
@property (nonatomic) BOOL receivingReport;
@@ -482,6 +503,27 @@
id _systemTimeChangeObserverToken;
NSMutableSet<MTRDeviceDelegateInfo *> * _delegates;
+
+ // Protects mutable state used by our description getter. This is a separate lock from "lock"
+ // so that we don't need to worry about getting our description while holding "lock" (e.g due to
+ // logging self). This lock _must_ be held narrowly, with no other lock acquisitions allowed
+ // while it's held, to avoid deadlock.
+ os_unfair_lock _descriptionLock;
+
+ // State used by our description getter: access to these must be protected by descriptionLock.
+ NSNumber * _Nullable _vid; // nil if unknown
+ NSNumber * _Nullable _pid; // nil if unknown
+ // _allNetworkFeatures is a bitwise or of the feature maps of all network commissioning clusters
+ // present on the device, or nil if there aren't any.
+ NSNumber * _Nullable _allNetworkFeatures;
+ // Copy of _internalDeviceState that is safe to use in description.
+ MTRInternalDeviceState _internalDeviceStateForDescription;
+ // Copy of _lastSubscriptionAttemptWait that is safe to use in description.
+ uint32_t _lastSubscriptionAttemptWaitForDescription;
+ // Most recent entry in _mostRecentReportTimes, if any.
+ NSDate * _Nullable _mostRecentReportTimeForDescription;
+ // Copy of _lastSubscriptionFailureTime that is safe to use in description.
+ NSDate * _Nullable _lastSubscriptionFailureTimeForDescription;
}
- (instancetype)initForSubclasses
@@ -498,6 +540,7 @@
if (self = [super init]) {
_lock = OS_UNFAIR_LOCK_INIT;
_timeSyncLock = OS_UNFAIR_LOCK_INIT;
+ _descriptionLock = OS_UNFAIR_LOCK_INIT;
_nodeID = [nodeID copy];
_fabricIndex = controller.fabricIndex;
_deviceController = controller;
@@ -507,6 +550,7 @@
_asyncWorkQueue = [[MTRAsyncWorkQueue alloc] initWithContext:self];
_state = MTRDeviceStateUnknown;
_internalDeviceState = MTRInternalDeviceStateUnsubscribed;
+ _internalDeviceStateForDescription = MTRInternalDeviceStateUnsubscribed;
if (controller.controllerDataStore) {
_persistedClusterData = [[NSCache alloc] init];
} else {
@@ -543,8 +587,58 @@
- (NSString *)description
{
+ id _Nullable vid;
+ id _Nullable pid;
+ NSNumber * _Nullable networkFeatures;
+ MTRInternalDeviceState internalDeviceState;
+ uint32_t lastSubscriptionAttemptWait;
+ NSDate * _Nullable mostRecentReportTime;
+ NSDate * _Nullable lastSubscriptionFailureTime;
+ {
+ std::lock_guard lock(_descriptionLock);
+ vid = _vid;
+ pid = _pid;
+ networkFeatures = _allNetworkFeatures;
+ internalDeviceState = _internalDeviceStateForDescription;
+ lastSubscriptionAttemptWait = _lastSubscriptionAttemptWaitForDescription;
+ mostRecentReportTime = _mostRecentReportTimeForDescription;
+ lastSubscriptionFailureTime = _lastSubscriptionFailureTimeForDescription;
+ }
+
+ if (vid == nil) {
+ vid = @"Unknown";
+ }
+
+ if (pid == nil) {
+ pid = @"Unknown";
+ }
+
+ NSString * wifi;
+ NSString * thread;
+ if (networkFeatures == nil) {
+ wifi = @"NO";
+ thread = @"NO";
+ } else {
+ wifi = YES_NO(networkFeatures.unsignedLongLongValue & MTRNetworkCommissioningFeatureWiFiNetworkInterface);
+ thread = YES_NO(networkFeatures.unsignedLongLongValue & MTRNetworkCommissioningFeatureThreadNetworkInterface);
+ }
+
+ NSString * reportAge;
+ if (mostRecentReportTime) {
+ reportAge = [NSString stringWithFormat:@" (%.0lfs ago)", -[mostRecentReportTime timeIntervalSinceNow]];
+ } else {
+ reportAge = @"";
+ }
+
+ NSString * subscriptionFailureAge;
+ if (lastSubscriptionFailureTime) {
+ subscriptionFailureAge = [NSString stringWithFormat:@" (%.0lfs ago)", -[lastSubscriptionFailureTime timeIntervalSinceNow]];
+ } else {
+ subscriptionFailureAge = @"";
+ }
+
return [NSString
- stringWithFormat:@"<MTRDevice: %p>[fabric: %u, nodeID: 0x%016llX]", self, _fabricIndex, _nodeID.unsignedLongLongValue];
+ stringWithFormat:@"<MTRDevice: %p, node: %016llX-%016llX (%llu), VID: %@, PID: %@, WiFi: %@, Thread: %@, state: %@, last subscription attempt wait: %lus, queued work: %lu, last report: %@%@, last subscription failure: %@%@, controller: %@>", self, _deviceController.compressedFabricID.unsignedLongLongValue, _nodeID.unsignedLongLongValue, _nodeID.unsignedLongLongValue, vid, pid, wifi, thread, InternalDeviceStateString(internalDeviceState), static_cast<unsigned long>(lastSubscriptionAttemptWait), static_cast<unsigned long>(_asyncWorkQueue.itemCount), mostRecentReportTime, reportAge, lastSubscriptionFailureTime, subscriptionFailureAge, _deviceController.uniqueIdentifier];
}
+ (MTRDevice *)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
@@ -661,13 +755,11 @@
- (NSArray<NSNumber *> *)_endpointsWithTimeSyncClusterServer
{
- auto partsList = [self readAttributeWithEndpointID:@(0) clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID) params:nil];
- NSMutableArray<NSNumber *> * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList];
- if (!endpointsOnDevice) {
- endpointsOnDevice = [[NSMutableArray<NSNumber *> alloc] init];
+ NSArray<NSNumber *> * endpointsOnDevice;
+ {
+ std::lock_guard lock(_lock);
+ endpointsOnDevice = [self _endpointList];
}
- // Add Root node!
- [endpointsOnDevice addObject:@(0)];
NSMutableArray<NSNumber *> * endpointsWithTimeSyncCluster = [[NSMutableArray<NSNumber *> alloc] init];
for (NSNumber * endpoint in endpointsOnDevice) {
@@ -1160,6 +1252,10 @@
os_unfair_lock_assert_owner(&self->_lock);
MTRInternalDeviceState lastState = _internalDeviceState;
_internalDeviceState = state;
+ {
+ std::lock_guard lock(_descriptionLock);
+ _internalDeviceStateForDescription = _internalDeviceState;
+ }
if (lastState != state) {
MTR_LOG("%@ internal state change %lu => %lu", self, static_cast<unsigned long>(lastState), static_cast<unsigned long>(state));
@@ -1201,7 +1297,7 @@
[self _stopConnectivityMonitoring];
// reset subscription attempt wait time when subscription succeeds
- _lastSubscriptionAttemptWait = 0;
+ [self _setLastSubscriptionAttemptWait:0];
auto initialSubscribeStart = _initialSubscribeStart;
// We no longer need to track subscribe latency for this device.
@@ -1367,6 +1463,10 @@
// former case we recently had a subscription and do not want to be forcing
// retries immediately.
_lastSubscriptionFailureTime = [NSDate now];
+ {
+ std::lock_guard lock(_descriptionLock);
+ _lastSubscriptionFailureTimeForDescription = _lastSubscriptionFailureTime;
+ }
deviceUsesThread = [self _deviceUsesThread];
@@ -1408,6 +1508,15 @@
[self _doHandleSubscriptionReset:retryDelay];
}
+- (void)_setLastSubscriptionAttemptWait:(uint32_t)lastSubscriptionAttemptWait
+{
+ os_unfair_lock_assert_owner(&_lock);
+ _lastSubscriptionAttemptWait = lastSubscriptionAttemptWait;
+
+ std::lock_guard lock(_descriptionLock);
+ _lastSubscriptionAttemptWaitForDescription = lastSubscriptionAttemptWait;
+}
+
- (void)_doHandleSubscriptionReset:(NSNumber * _Nullable)retryDelay
{
os_unfair_lock_assert_owner(&_lock);
@@ -1417,6 +1526,10 @@
// has given up completely. Those all count as "we have tried and failed to
// subscribe".
_lastSubscriptionFailureTime = [NSDate now];
+ {
+ std::lock_guard lock(_descriptionLock);
+ _lastSubscriptionFailureTimeForDescription = _lastSubscriptionFailureTime;
+ }
// if there is no delegate then also do not retry
if (![self _delegateExists]) {
@@ -1441,16 +1554,17 @@
// counter, so that we don't end up waiting for a long time if the next
// attempt fails for some reason, and retry after whatever time period
// the device told us to use.
- _lastSubscriptionAttemptWait = 0;
+ [self _setLastSubscriptionAttemptWait:0];
secondsToWait = retryDelay.doubleValue;
MTR_LOG("%@ resetting resubscribe attempt counter, and delaying by the server-provided delay: %f",
self, secondsToWait);
} else {
- _lastSubscriptionAttemptWait *= 2;
- if (_lastSubscriptionAttemptWait > MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS) {
- _lastSubscriptionAttemptWait = MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS;
+ auto lastSubscriptionAttemptWait = _lastSubscriptionAttemptWait * 2;
+ if (lastSubscriptionAttemptWait > MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS) {
+ lastSubscriptionAttemptWait = MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS;
}
- secondsToWait = _lastSubscriptionAttemptWait;
+ [self _setLastSubscriptionAttemptWait:lastSubscriptionAttemptWait];
+ secondsToWait = lastSubscriptionAttemptWait;
}
MTR_LOG("%@ scheduling to reattempt subscription in %f seconds", self, secondsToWait);
@@ -1665,6 +1779,9 @@
- (void)unitTestSetMostRecentReportTimes:(NSMutableArray<NSDate *> *)mostRecentReportTimes
{
_mostRecentReportTimes = mostRecentReportTimes;
+
+ std::lock_guard lock(_descriptionLock);
+ _mostRecentReportTimeForDescription = [mostRecentReportTimes lastObject];
}
#endif
@@ -1719,6 +1836,11 @@
}
[_mostRecentReportTimes addObject:[NSDate now]];
+ {
+ std::lock_guard lock(_descriptionLock);
+ _mostRecentReportTimeForDescription = [_mostRecentReportTimes lastObject];
+ }
+
// Calculate running average and update multiplier - need at least 2 items to calculate intervals
if (_mostRecentReportTimes.count > 2) {
NSTimeInterval cumulativeIntervals = 0;
@@ -1784,6 +1906,10 @@
_clusterDataPersistenceFirstScheduledTime = nil;
_mostRecentReportTimes = nil;
+ {
+ std::lock_guard lock(_descriptionLock);
+ _mostRecentReportTimeForDescription = nil;
+ }
_deviceReportingExcessivelyStartTime = nil;
_reportToPersistenceDelayCurrentMultiplier = 1;
@@ -2236,6 +2362,10 @@
[clusterData storeValue:value forAttribute:path.attribute];
+ if ([self _attributePathAffectsDescriptionData:path]) {
+ [self _updateAttributeDependentDescriptionData];
+ }
+
if (value != nil
&& isFromSubscription
&& !_receivingPrimingReport
@@ -2295,10 +2425,11 @@
- (void)_setupConnectivityMonitoring
{
#if ENABLE_CONNECTIVITY_MONITORING
- // Dispatch to own queue first to avoid deadlock with syncGetCompressedFabricID
+ // Dispatch to own queue because we used to need to do that to get the compressedFabricID, but
+ // at this point that's not really needed anymore.
dispatch_async(self.queue, ^{
// Get the required info before setting up the connectivity monitor
- NSNumber * compressedFabricID = [self->_deviceController syncGetCompressedFabricID];
+ NSNumber * compressedFabricID = [self->_deviceController compressedFabricID];
if (!compressedFabricID) {
MTR_LOG_ERROR("%@ could not get compressed fabricID", self);
return;
@@ -3652,7 +3783,7 @@
if (!_deviceConfigurationChanged) {
_deviceConfigurationChanged = [self _attributeAffectsDeviceConfiguration:attributePath];
if (_deviceConfigurationChanged) {
- MTR_LOG("Device configuration changed due to changes in attribute %@", attributePath);
+ MTR_LOG("%@ device configuration changed due to changes in attribute %@", self, attributePath);
}
}
@@ -3761,6 +3892,8 @@
[_persistedClusterData setObject:clusterData[clusterPath] forKey:clusterPath];
}
+ [self _updateAttributeDependentDescriptionData];
+
// We have some stored data. Since we don't store data until the end of the
// initial priming report, our device cache must be primed.
_deviceCachePrimed = YES;
@@ -4104,6 +4237,10 @@
{
auto * cachedData = [self _cachedAttributeValueForPath:attributePath];
+ if (cachedData == nil) {
+ return nil;
+ }
+
auto * attrReport = [[MTRAttributeReport alloc] initWithResponseValue:@{
MTRAttributePathKey : attributePath,
MTRDataKey : cachedData,
@@ -4133,6 +4270,8 @@
- (void)_addInformationalAttributesToCurrentMetricScope
{
+ os_unfair_lock_assert_owner(&self->_lock);
+
using namespace chip::Tracing::DarwinFramework;
MATTER_LOG_METRIC(kMetricDeviceVendorID, [self _informationalVendorID].unsignedShortValue);
MATTER_LOG_METRIC(kMetricDeviceProductID, [self _informationalProductID].unsignedShortValue);
@@ -4140,6 +4279,89 @@
MATTER_LOG_METRIC(kMetricDeviceUsesThread, usesThread);
}
+#pragma mark - Description handling
+
+- (BOOL)_attributePathAffectsDescriptionData:(MTRAttributePath *)path
+{
+ // Technically this does not need to be called while locked, but in
+ // practice it is, and we want to make sure it's clear that this function
+ // should never start taking our data lock.
+ os_unfair_lock_assert_owner(&_lock);
+
+ switch (path.cluster.unsignedLongLongValue) {
+ case MTRClusterIDTypeBasicInformationID: {
+ switch (path.attribute.unsignedLongLongValue) {
+ case MTRAttributeIDTypeClusterBasicInformationAttributeVendorIDID:
+ case MTRAttributeIDTypeClusterBasicInformationAttributeProductIDID:
+ return YES;
+ default:
+ return NO;
+ }
+ }
+ case MTRClusterIDTypeNetworkCommissioningID: {
+ return path.attribute.unsignedLongLongValue == MTRAttributeIDTypeGlobalAttributeFeatureMapID;
+ }
+ default:
+ return NO;
+ }
+}
+
+- (void)_updateAttributeDependentDescriptionData
+{
+ os_unfair_lock_assert_owner(&_lock);
+
+ NSNumber * _Nullable vid = [self _informationalVendorID];
+ NSNumber * _Nullable pid = [self _informationalProductID];
+ NSNumber * _Nullable networkFeatures = [self _networkFeatures];
+
+ std::lock_guard lock(_descriptionLock);
+ _vid = vid;
+ _pid = pid;
+ _allNetworkFeatures = networkFeatures;
+}
+
+- (NSArray<NSNumber *> *)_endpointList
+{
+ os_unfair_lock_assert_owner(&_lock);
+
+ auto * partsListPath = [MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId)
+ clusterID:@(MTRClusterIDTypeDescriptorID)
+ attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)];
+ auto * partsList = [self _cachedAttributeValueForPath:partsListPath];
+ NSMutableArray<NSNumber *> * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList];
+ if (!endpointsOnDevice) {
+ endpointsOnDevice = [[NSMutableArray<NSNumber *> alloc] init];
+ }
+ // Add Root node!
+ [endpointsOnDevice addObject:@(0)];
+ return endpointsOnDevice;
+}
+
+- (NSNumber * _Nullable)_networkFeatures
+{
+ NSNumber * _Nullable result = nil;
+ auto * endpoints = [self _endpointList];
+ for (NSNumber * endpoint in endpoints) {
+ auto * featureMapPath = [MTRAttributePath attributePathWithEndpointID:endpoint
+ clusterID:@(MTRClusterIDTypeNetworkCommissioningID)
+ attributeID:@(MTRAttributeIDTypeGlobalAttributeFeatureMapID)];
+ auto * featureMap = [self _informationalNumberAtAttributePath:featureMapPath];
+ if (featureMap == nil) {
+ // No network commissioning cluster on this endpoint, or no known
+ // FeatureMap attribute value for it yet.
+ continue;
+ }
+
+ if (result == nil) {
+ result = featureMap;
+ } else {
+ result = @(featureMap.unsignedLongLongValue | result.unsignedLongLongValue);
+ }
+ }
+
+ return result;
+}
+
@end
/* BEGIN DRAGONS: Note methods here cannot be renamed, and are used by private callers, do not rename, remove or modify behavior here */
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController.mm b/src/darwin/Framework/CHIP/MTRDeviceController.mm
index 6c312d5..25a0e7d 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController.mm
+++ b/src/darwin/Framework/CHIP/MTRDeviceController.mm
@@ -75,6 +75,7 @@
#include <atomic>
#include <dns_sd.h>
+#include <optional>
#include <string>
#import <os/lock.h>
@@ -107,8 +108,9 @@
using namespace chip::Tracing::DarwinFramework;
@implementation MTRDeviceController {
- // Atomic because it can be touched from multiple threads.
+ // Atomic because they can be touched from multiple threads.
std::atomic<chip::FabricIndex> _storedFabricIndex;
+ std::atomic<std::optional<uint64_t>> _storedCompressedFabricID;
// queue used to serialize all work performed by the MTRDeviceController
dispatch_queue_t _chipWorkQueue;
@@ -278,6 +280,7 @@
_concurrentSubscriptionPool = [[MTRAsyncWorkQueue alloc] initWithContext:self width:concurrentSubscriptionPoolSize];
_storedFabricIndex = chip::kUndefinedFabricIndex;
+ _storedCompressedFabricID = std::nullopt;
_storageBehaviorConfiguration = storageBehaviorConfiguration;
}
@@ -351,6 +354,7 @@
// shutdown completes, in case it wants to write to storage as it
// shuts down.
_storedFabricIndex = chip::kUndefinedFabricIndex;
+ _storedCompressedFabricID = std::nullopt;
delete commissionerToShutDown;
if (_operationalCredentialsDelegate != nil) {
_operationalCredentialsDelegate->SetDeviceCommissioner(nullptr);
@@ -590,6 +594,7 @@
}
self->_storedFabricIndex = fabricIdx;
+ self->_storedCompressedFabricID = _cppCommissioner->GetCompressedFabricId();
commissionerInitialized = YES;
MTR_LOG("%@ startup succeeded for nodeID 0x%016llX", self, self->_cppCommissioner->GetNodeId());
@@ -1440,20 +1445,8 @@
- (nullable NSNumber *)compressedFabricID
{
- assertChipStackLockedByCurrentThread();
-
- if (!_cppCommissioner) {
- return nil;
- }
-
- return @(_cppCommissioner->GetCompressedFabricId());
-}
-
-- (NSNumber * _Nullable)syncGetCompressedFabricID
-{
- return [self syncRunOnWorkQueueWithReturnValue:^NSNumber * {
- return [self compressedFabricID];
- } error:nil];
+ auto storedValue = _storedCompressedFabricID.load();
+ return storedValue.has_value() ? @(storedValue.value()) : nil;
}
- (CHIP_ERROR)isRunningOnFabric:(chip::FabricTable *)fabricTable
diff --git a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
index 925bbb2..479dd85 100644
--- a/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
+++ b/src/darwin/Framework/CHIP/MTRDeviceController_Internal.h
@@ -79,8 +79,6 @@
/**
* Will return the compressed fabric id of the fabric if the controller is
* running, else nil.
- *
- * This property MUST be gotten from the Matter work queue.
*/
@property (nonatomic, readonly, nullable) NSNumber * compressedFabricID;
@@ -266,8 +264,6 @@
- (MTRDevice *)deviceForNodeID:(NSNumber *)nodeID;
- (void)removeDevice:(MTRDevice *)device;
-- (NSNumber * _Nullable)syncGetCompressedFabricID;
-
/**
* Since getSessionForNode now enqueues by the subscription pool for Thread
* devices, MTRDevice needs a direct non-queued access because it already
diff --git a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
index a90b49d..e22b0b8 100644
--- a/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
+++ b/src/darwin/Framework/CHIP/MTRDevice_Concrete.mm
@@ -219,6 +219,24 @@
{
return state == MTRInternalDeviceStateInitialSubscriptionEstablished || state == MTRInternalDeviceStateLaterSubscriptionEstablished;
}
+
+NSString * InternalDeviceStateString(MTRInternalDeviceState state)
+{
+ switch (state) {
+ case MTRInternalDeviceStateUnsubscribed:
+ return @"Unsubscribed";
+ case MTRInternalDeviceStateSubscribing:
+ return @"Subscribing";
+ case MTRInternalDeviceStateInitialSubscriptionEstablished:
+ return @"InitialSubscriptionEstablished";
+ case MTRInternalDeviceStateResubscribing:
+ return @"Resubscribing";
+ case MTRInternalDeviceStateLaterSubscriptionEstablished:
+ return @"LaterSubscriptionEstablished";
+ default:
+ return @"Unknown";
+ }
+}
} // anonymous namespace
typedef NS_ENUM(NSUInteger, MTRDeviceExpectedValueFieldIndex) {
@@ -265,8 +283,11 @@
@interface MTRDevice_Concrete ()
@property (nonatomic, readonly) os_unfair_lock lock; // protects the caches and device state
// protects against concurrent time updates by guarding timeUpdateScheduled flag which manages time updates scheduling,
-// and protects device calls to setUTCTime and setDSTOffset
+// and protects device calls to setUTCTime and setDSTOffset. This can't just be replaced with "lock", because the time
+// update code calls public APIs like readAttributeWithEndpointID:.. (which attempt to take "lock") while holding
+// whatever lock protects the time sync bits.
@property (nonatomic, readonly) os_unfair_lock timeSyncLock;
+
@property (nonatomic) chip::FabricIndex fabricIndex;
@property (nonatomic) NSMutableArray<NSDictionary<NSString *, id> *> * unreportedEvents;
@property (nonatomic) BOOL receivingReport;
@@ -392,6 +413,27 @@
id _systemTimeChangeObserverToken;
NSMutableSet<MTRDeviceDelegateInfo_ConcreteCopy *> * _delegates;
+
+ // Protects mutable state used by our description getter. This is a separate lock from "lock"
+ // so that we don't need to worry about getting our description while holding "lock" (e.g due to
+ // logging self). This lock _must_ be held narrowly, with no other lock acquisitions allowed
+ // while it's held, to avoid deadlock.
+ os_unfair_lock _descriptionLock;
+
+ // State used by our description getter: access to these must be protected by descriptionLock.
+ NSNumber * _Nullable _vid; // nil if unknown
+ NSNumber * _Nullable _pid; // nil if unknown
+ // _allNetworkFeatures is a bitwise or of the feature maps of all network commissioning clusters
+ // present on the device, or nil if there aren't any.
+ NSNumber * _Nullable _allNetworkFeatures;
+ // Copy of _internalDeviceState that is safe to use in description.
+ MTRInternalDeviceState _internalDeviceStateForDescription;
+ // Copy of _lastSubscriptionAttemptWait that is safe to use in description.
+ uint32_t _lastSubscriptionAttemptWaitForDescription;
+ // Most recent entry in _mostRecentReportTimes, if any.
+ NSDate * _Nullable _mostRecentReportTimeForDescription;
+ // Copy of _lastSubscriptionFailureTime that is safe to use in description.
+ NSDate * _Nullable _lastSubscriptionFailureTimeForDescription;
}
// synthesize superclass property readwrite accessors
@@ -411,6 +453,7 @@
if (self = [super initForSubclasses]) {
_lock = OS_UNFAIR_LOCK_INIT;
_timeSyncLock = OS_UNFAIR_LOCK_INIT;
+ _descriptionLock = OS_UNFAIR_LOCK_INIT;
_nodeID = [nodeID copy];
_fabricIndex = controller.fabricIndex;
_deviceController = controller;
@@ -420,6 +463,7 @@
_asyncWorkQueue = [[MTRAsyncWorkQueue alloc] initWithContext:self];
_state = MTRDeviceStateUnknown;
_internalDeviceState = MTRInternalDeviceStateUnsubscribed;
+ _internalDeviceStateForDescription = MTRInternalDeviceStateUnsubscribed;
if (controller.controllerDataStore) {
_persistedClusterData = [[NSCache alloc] init];
} else {
@@ -456,8 +500,58 @@
- (NSString *)description
{
+ id _Nullable vid;
+ id _Nullable pid;
+ NSNumber * _Nullable networkFeatures;
+ MTRInternalDeviceState internalDeviceState;
+ uint32_t lastSubscriptionAttemptWait;
+ NSDate * _Nullable mostRecentReportTime;
+ NSDate * _Nullable lastSubscriptionFailureTime;
+ {
+ std::lock_guard lock(_descriptionLock);
+ vid = _vid;
+ pid = _pid;
+ networkFeatures = _allNetworkFeatures;
+ internalDeviceState = _internalDeviceStateForDescription;
+ lastSubscriptionAttemptWait = _lastSubscriptionAttemptWaitForDescription;
+ mostRecentReportTime = _mostRecentReportTimeForDescription;
+ lastSubscriptionFailureTime = _lastSubscriptionFailureTimeForDescription;
+ }
+
+ if (vid == nil) {
+ vid = @"Unknown";
+ }
+
+ if (pid == nil) {
+ pid = @"Unknown";
+ }
+
+ NSString * wifi;
+ NSString * thread;
+ if (networkFeatures == nil) {
+ wifi = @"NO";
+ thread = @"NO";
+ } else {
+ wifi = YES_NO(networkFeatures.unsignedLongLongValue & MTRNetworkCommissioningFeatureWiFiNetworkInterface);
+ thread = YES_NO(networkFeatures.unsignedLongLongValue & MTRNetworkCommissioningFeatureThreadNetworkInterface);
+ }
+
+ NSString * reportAge;
+ if (mostRecentReportTime) {
+ reportAge = [NSString stringWithFormat:@" (%.0lfs ago)", -[mostRecentReportTime timeIntervalSinceNow]];
+ } else {
+ reportAge = @"";
+ }
+
+ NSString * subscriptionFailureAge;
+ if (lastSubscriptionFailureTime) {
+ subscriptionFailureAge = [NSString stringWithFormat:@" (%.0lfs ago)", -[lastSubscriptionFailureTime timeIntervalSinceNow]];
+ } else {
+ subscriptionFailureAge = @"";
+ }
+
return [NSString
- stringWithFormat:@"<MTRDevice: %p>[fabric: %u, nodeID: 0x%016llX]", self, _fabricIndex, _nodeID.unsignedLongLongValue];
+ stringWithFormat:@"<MTRDevice: %p, node: %016llX-%016llX (%llu), VID: %@, PID: %@, WiFi: %@, Thread: %@, state: %@, last subscription attempt wait: %lus, queued work: %lu, last report: %@%@, last subscription failure: %@%@, controller: %@>", self, _deviceController.compressedFabricID.unsignedLongLongValue, _nodeID.unsignedLongLongValue, _nodeID.unsignedLongLongValue, vid, pid, wifi, thread, InternalDeviceStateString(internalDeviceState), static_cast<unsigned long>(lastSubscriptionAttemptWait), static_cast<unsigned long>(_asyncWorkQueue.itemCount), mostRecentReportTime, reportAge, lastSubscriptionFailureTime, subscriptionFailureAge, _deviceController.uniqueIdentifier];
}
+ (MTRDevice *)deviceWithNodeID:(NSNumber *)nodeID controller:(MTRDeviceController *)controller
@@ -574,13 +668,11 @@
- (NSArray<NSNumber *> *)_endpointsWithTimeSyncClusterServer
{
- auto partsList = [self readAttributeWithEndpointID:@(0) clusterID:@(MTRClusterIDTypeDescriptorID) attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID) params:nil];
- NSMutableArray<NSNumber *> * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList];
- if (!endpointsOnDevice) {
- endpointsOnDevice = [[NSMutableArray<NSNumber *> alloc] init];
+ NSArray<NSNumber *> * endpointsOnDevice;
+ {
+ std::lock_guard lock(_lock);
+ endpointsOnDevice = [self _endpointList];
}
- // Add Root node!
- [endpointsOnDevice addObject:@(0)];
NSMutableArray<NSNumber *> * endpointsWithTimeSyncCluster = [[NSMutableArray<NSNumber *> alloc] init];
for (NSNumber * endpoint in endpointsOnDevice) {
@@ -1073,6 +1165,10 @@
os_unfair_lock_assert_owner(&self->_lock);
MTRInternalDeviceState lastState = _internalDeviceState;
_internalDeviceState = state;
+ {
+ std::lock_guard lock(_descriptionLock);
+ _internalDeviceStateForDescription = _internalDeviceState;
+ }
if (lastState != state) {
MTR_LOG("%@ internal state change %lu => %lu", self, static_cast<unsigned long>(lastState), static_cast<unsigned long>(state));
@@ -1114,7 +1210,7 @@
[self _stopConnectivityMonitoring];
// reset subscription attempt wait time when subscription succeeds
- _lastSubscriptionAttemptWait = 0;
+ [self _setLastSubscriptionAttemptWait:0];
auto initialSubscribeStart = _initialSubscribeStart;
// We no longer need to track subscribe latency for this device.
@@ -1280,6 +1376,10 @@
// former case we recently had a subscription and do not want to be forcing
// retries immediately.
_lastSubscriptionFailureTime = [NSDate now];
+ {
+ std::lock_guard lock(_descriptionLock);
+ _lastSubscriptionFailureTimeForDescription = _lastSubscriptionFailureTime;
+ }
deviceUsesThread = [self _deviceUsesThread];
@@ -1321,6 +1421,15 @@
[self _doHandleSubscriptionReset:retryDelay];
}
+- (void)_setLastSubscriptionAttemptWait:(uint32_t)lastSubscriptionAttemptWait
+{
+ os_unfair_lock_assert_owner(&_lock);
+ _lastSubscriptionAttemptWait = lastSubscriptionAttemptWait;
+
+ std::lock_guard lock(_descriptionLock);
+ _lastSubscriptionAttemptWaitForDescription = lastSubscriptionAttemptWait;
+}
+
- (void)_doHandleSubscriptionReset:(NSNumber * _Nullable)retryDelay
{
os_unfair_lock_assert_owner(&_lock);
@@ -1330,6 +1439,10 @@
// has given up completely. Those all count as "we have tried and failed to
// subscribe".
_lastSubscriptionFailureTime = [NSDate now];
+ {
+ std::lock_guard lock(_descriptionLock);
+ _lastSubscriptionFailureTimeForDescription = _lastSubscriptionFailureTime;
+ }
// if there is no delegate then also do not retry
if (![self _delegateExists]) {
@@ -1354,16 +1467,17 @@
// counter, so that we don't end up waiting for a long time if the next
// attempt fails for some reason, and retry after whatever time period
// the device told us to use.
- _lastSubscriptionAttemptWait = 0;
+ [self _setLastSubscriptionAttemptWait:0];
secondsToWait = retryDelay.doubleValue;
MTR_LOG("%@ resetting resubscribe attempt counter, and delaying by the server-provided delay: %f",
self, secondsToWait);
} else {
- _lastSubscriptionAttemptWait *= 2;
- if (_lastSubscriptionAttemptWait > MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS) {
- _lastSubscriptionAttemptWait = MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS;
+ auto lastSubscriptionAttemptWait = _lastSubscriptionAttemptWait * 2;
+ if (lastSubscriptionAttemptWait > MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS) {
+ lastSubscriptionAttemptWait = MTRDEVICE_SUBSCRIPTION_ATTEMPT_MAX_WAIT_SECONDS;
}
- secondsToWait = _lastSubscriptionAttemptWait;
+ [self _setLastSubscriptionAttemptWait:lastSubscriptionAttemptWait];
+ secondsToWait = lastSubscriptionAttemptWait;
}
MTR_LOG("%@ scheduling to reattempt subscription in %f seconds", self, secondsToWait);
@@ -1578,6 +1692,9 @@
- (void)unitTestSetMostRecentReportTimes:(NSMutableArray<NSDate *> *)mostRecentReportTimes
{
_mostRecentReportTimes = mostRecentReportTimes;
+
+ std::lock_guard lock(_descriptionLock);
+ _mostRecentReportTimeForDescription = [mostRecentReportTimes lastObject];
}
#endif
@@ -1632,6 +1749,11 @@
}
[_mostRecentReportTimes addObject:[NSDate now]];
+ {
+ std::lock_guard lock(_descriptionLock);
+ _mostRecentReportTimeForDescription = [_mostRecentReportTimes lastObject];
+ }
+
// Calculate running average and update multiplier - need at least 2 items to calculate intervals
if (_mostRecentReportTimes.count > 2) {
NSTimeInterval cumulativeIntervals = 0;
@@ -1697,6 +1819,10 @@
_clusterDataPersistenceFirstScheduledTime = nil;
_mostRecentReportTimes = nil;
+ {
+ std::lock_guard lock(_descriptionLock);
+ _mostRecentReportTimeForDescription = nil;
+ }
_deviceReportingExcessivelyStartTime = nil;
_reportToPersistenceDelayCurrentMultiplier = 1;
@@ -2149,6 +2275,10 @@
[clusterData storeValue:value forAttribute:path.attribute];
+ if ([self _attributePathAffectsDescriptionData:path]) {
+ [self _updateAttributeDependentDescriptionData];
+ }
+
if (value != nil
&& isFromSubscription
&& !_receivingPrimingReport
@@ -2208,10 +2338,11 @@
- (void)_setupConnectivityMonitoring
{
#if ENABLE_CONNECTIVITY_MONITORING
- // Dispatch to own queue first to avoid deadlock with syncGetCompressedFabricID
+ // Dispatch to own queue because we used to need to do that to get the compressedFabricID, but
+ // at this point that's not really needed anymore.
dispatch_async(self.queue, ^{
// Get the required info before setting up the connectivity monitor
- NSNumber * compressedFabricID = [self->_deviceController syncGetCompressedFabricID];
+ NSNumber * compressedFabricID = [self->_deviceController compressedFabricID];
if (!compressedFabricID) {
MTR_LOG_ERROR("%@ could not get compressed fabricID", self);
return;
@@ -3565,7 +3696,7 @@
if (!_deviceConfigurationChanged) {
_deviceConfigurationChanged = [self _attributeAffectsDeviceConfiguration:attributePath];
if (_deviceConfigurationChanged) {
- MTR_LOG("Device configuration changed due to changes in attribute %@", attributePath);
+ MTR_LOG("%@ device configuration changed due to changes in attribute %@", self, attributePath);
}
}
@@ -3674,6 +3805,8 @@
[_persistedClusterData setObject:clusterData[clusterPath] forKey:clusterPath];
}
+ [self _updateAttributeDependentDescriptionData];
+
// We have some stored data. Since we don't store data until the end of the
// initial priming report, our device cache must be primed.
_deviceCachePrimed = YES;
@@ -4017,6 +4150,10 @@
{
auto * cachedData = [self _cachedAttributeValueForPath:attributePath];
+ if (cachedData == nil) {
+ return nil;
+ }
+
auto * attrReport = [[MTRAttributeReport alloc] initWithResponseValue:@{
MTRAttributePathKey : attributePath,
MTRDataKey : cachedData,
@@ -4046,6 +4183,8 @@
- (void)_addInformationalAttributesToCurrentMetricScope
{
+ os_unfair_lock_assert_owner(&self->_lock);
+
using namespace chip::Tracing::DarwinFramework;
MATTER_LOG_METRIC(kMetricDeviceVendorID, [self _informationalVendorID].unsignedShortValue);
MATTER_LOG_METRIC(kMetricDeviceProductID, [self _informationalProductID].unsignedShortValue);
@@ -4053,6 +4192,89 @@
MATTER_LOG_METRIC(kMetricDeviceUsesThread, usesThread);
}
+#pragma mark - Description handling
+
+- (BOOL)_attributePathAffectsDescriptionData:(MTRAttributePath *)path
+{
+ // Technically this does not need to be called while locked, but in
+ // practice it is, and we want to make sure it's clear that this function
+ // should never start taking our data lock.
+ os_unfair_lock_assert_owner(&_lock);
+
+ switch (path.cluster.unsignedLongLongValue) {
+ case MTRClusterIDTypeBasicInformationID: {
+ switch (path.attribute.unsignedLongLongValue) {
+ case MTRAttributeIDTypeClusterBasicInformationAttributeVendorIDID:
+ case MTRAttributeIDTypeClusterBasicInformationAttributeProductIDID:
+ return YES;
+ default:
+ return NO;
+ }
+ }
+ case MTRClusterIDTypeNetworkCommissioningID: {
+ return path.attribute.unsignedLongLongValue == MTRAttributeIDTypeGlobalAttributeFeatureMapID;
+ }
+ default:
+ return NO;
+ }
+}
+
+- (void)_updateAttributeDependentDescriptionData
+{
+ os_unfair_lock_assert_owner(&_lock);
+
+ NSNumber * _Nullable vid = [self _informationalVendorID];
+ NSNumber * _Nullable pid = [self _informationalProductID];
+ NSNumber * _Nullable networkFeatures = [self _networkFeatures];
+
+ std::lock_guard lock(_descriptionLock);
+ _vid = vid;
+ _pid = pid;
+ _allNetworkFeatures = networkFeatures;
+}
+
+- (NSArray<NSNumber *> *)_endpointList
+{
+ os_unfair_lock_assert_owner(&_lock);
+
+ auto * partsListPath = [MTRAttributePath attributePathWithEndpointID:@(kRootEndpointId)
+ clusterID:@(MTRClusterIDTypeDescriptorID)
+ attributeID:@(MTRAttributeIDTypeClusterDescriptorAttributePartsListID)];
+ auto * partsList = [self _cachedAttributeValueForPath:partsListPath];
+ NSMutableArray<NSNumber *> * endpointsOnDevice = [self arrayOfNumbersFromAttributeValue:partsList];
+ if (!endpointsOnDevice) {
+ endpointsOnDevice = [[NSMutableArray<NSNumber *> alloc] init];
+ }
+ // Add Root node!
+ [endpointsOnDevice addObject:@(0)];
+ return endpointsOnDevice;
+}
+
+- (NSNumber * _Nullable)_networkFeatures
+{
+ NSNumber * _Nullable result = nil;
+ auto * endpoints = [self _endpointList];
+ for (NSNumber * endpoint in endpoints) {
+ auto * featureMapPath = [MTRAttributePath attributePathWithEndpointID:endpoint
+ clusterID:@(MTRClusterIDTypeNetworkCommissioningID)
+ attributeID:@(MTRAttributeIDTypeGlobalAttributeFeatureMapID)];
+ auto * featureMap = [self _informationalNumberAtAttributePath:featureMapPath];
+ if (featureMap == nil) {
+ // No network commissioning cluster on this endpoint, or no known
+ // FeatureMap attribute value for it yet.
+ continue;
+ }
+
+ if (result == nil) {
+ result = featureMap;
+ } else {
+ result = @(featureMap.unsignedLongLongValue | result.unsignedLongLongValue);
+ }
+ }
+
+ return result;
+}
+
@end
/* BEGIN DRAGONS: Note methods here cannot be renamed, and are used by private callers, do not rename, remove or modify behavior here */