blob: 606bd2dbfbc9b5d94e7c5c93e0424dd66438edc3 [file] [log] [blame]
/**
*
* Copyright (c) 2024 Project CHIP Authors
*
* 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.
*/
#import "MTRDiagnosticLogsDownloader.h"
#import <Matter/Matter.h>
#include <platform/CHIPDeviceLayer.h>
#include <platform/LockTracker.h>
#include <protocols/bdx/BdxTransferServerDelegate.h>
#include <protocols/bdx/DiagnosticLogs.h>
#import "MTRDeviceController_Internal.h"
#import "MTRError_Internal.h"
#import "MTRLogging_Internal.h"
#import "NSDataSpanConversion.h"
#import "NSStringSpanConversion.h"
#import "zap-generated/MTRClusters.h"
typedef void (^AbortHandler)(NSError * error);
static NSString * const kErrorInitDiagnosticLogsDownloader = @"Init failure while initializing Diagnostic Logs bridge.";
static NSString * const kEndUserSupport = @"EndUserSupport";
static NSString * const kNetworkDiagnostics = @"NetworkDiagnostics";
static NSString * const kCrash = @"Crash";
constexpr uint8_t kDiagnosticLogsEndPoint = 0;
class DiagnosticLogsDownloaderBridge;
NS_ASSUME_NONNULL_BEGIN
@interface MTRDownload : NSObject
@property (nonatomic) NSString * fileDesignator;
@property (nonatomic) NSNumber * fabricIndex;
@property (nonatomic) NSNumber * nodeID;
@property (nonatomic) NSURL * fileURL;
@property (nonatomic) NSFileHandle * fileHandle;
@property (nonatomic) AbortHandler abortHandler;
@property (nonatomic) MTRStatusCompletion finalize;
- (instancetype)initWithType:(MTRDiagnosticLogType)type
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
done:(void (^)(MTRDownload * finishedDownload))done;
- (void)writeToFile:(NSData *)data error:(out NSError **)error;
- (BOOL)matches:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID;
- (void)checkInteractionModelResponse:(MTRDiagnosticLogsClusterRetrieveLogsResponseParams * _Nullable)response error:(NSError * _Nullable)error;
- (void)success;
- (void)failure:(NSError * _Nullable)error;
@end
@interface MTRDownloads : NSObject
@property (nonatomic, strong) NSMutableArray<MTRDownload *> * downloads;
- (MTRDownload * _Nullable)get:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID;
- (MTRDownload * _Nullable)add:(MTRDiagnosticLogType)type
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
done:(void (^)(MTRDownload * finishedDownload))done;
- (void)abortDownloadsForController:(MTRDeviceController_Concrete *)controller;
@end
@interface MTRDiagnosticLogsDownloader ()
@property (readonly) DiagnosticLogsDownloaderBridge * bridge;
@property (nonatomic, strong) MTRDownloads * downloads;
/**
* Notify the delegate when a BDX Session starts for some logs.
*
* If completion is passed a non-nil error, that will be converted into
* an error response to the BDX initiatior. Otherwise a success response will be sent.
*/
- (void)handleBDXTransferSessionBeginForFileDesignator:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
completion:(MTRStatusCompletion)completion
abortHandler:(AbortHandler)abortHandler;
/**
* Notify the delegate when some data is received on the BDX Session.
*
* If completion is passed a non-nil error, that will be converted into
* an error response to the sender. Otherwise a success response will be sent.
*/
- (void)handleBDXTransferSessionDataForFileDesignator:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
data:(NSData *)data
completion:(MTRStatusCompletion)completion;
/**
* Notify the delegate when a BDX Session ends for some logs.
*/
- (void)handleBDXTransferSessionEndForFileDesignator:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
error:(NSError * _Nullable)error;
@end
NS_ASSUME_NONNULL_END
class DiagnosticLogsDownloaderBridge : public chip::bdx::BDXTransferServerDelegate {
public:
DiagnosticLogsDownloaderBridge(MTRDiagnosticLogsDownloader * delegate);
~DiagnosticLogsDownloaderBridge();
/////////// BDXTransferServerDelegate Interface /////////
CHIP_ERROR OnTransferBegin(chip::bdx::BDXTransferProxy * transfer) override;
CHIP_ERROR OnTransferEnd(chip::bdx::BDXTransferProxy * transfer, CHIP_ERROR error) override;
CHIP_ERROR OnTransferData(chip::bdx::BDXTransferProxy * transfer, const chip::ByteSpan & data) override;
CHIP_ERROR StartBDXTransferTimeout(MTRDownload * download, uint16_t timeoutInSeconds);
void CancelBDXTransferTimeout(MTRDownload * download);
private:
static void OnTransferTimeout(chip::System::Layer * layer, void * context);
MTRDiagnosticLogsDownloader * __weak mDelegate;
};
@implementation MTRDownload
- (instancetype)initWithType:(MTRDiagnosticLogType)type
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
done:(void (^)(MTRDownload * finishedDownload))done;
{
self = [super init];
if (self) {
auto * fileDesignator = [self _toFileDesignatorString:type nodeID:nodeID];
auto * fileURL = [self _toFileURL:type nodeID:nodeID];
__weak typeof(self) weakSelf = self;
auto bdxTransferDone = ^(NSError * bdxError) {
dispatch_async(queue, ^{
MTRDownload * strongSelf = weakSelf;
if (strongSelf) {
// If a fileHandle exists, it means that the BDX session has been initiated and a file has
// been created to host the data of the session. So even if there is an error there may be some
// data in the logs that the caller may find useful. For this reason, fileURL is passed in even
// when there is an error but fileHandle is not nil.
completion(strongSelf->_fileHandle ? fileURL : nil, bdxError);
done(strongSelf);
}
});
};
_fileDesignator = fileDesignator;
_fabricIndex = fabricIndex;
_nodeID = nodeID;
_fileURL = fileURL;
_fileHandle = nil;
_finalize = bdxTransferDone;
}
return self;
}
- (void)checkInteractionModelResponse:(MTRDiagnosticLogsClusterRetrieveLogsResponseParams * _Nullable)response error:(NSError * _Nullable)error
{
VerifyOrReturn(nil == error, [self failure:error]);
auto status = response.status;
VerifyOrReturn(![status isEqual:@(MTRDiagnosticLogsStatusBusy)], [self failure:[MTRError errorForCHIPErrorCode:CHIP_ERROR_BUSY]]);
VerifyOrReturn(![status isEqual:@(MTRDiagnosticLogsStatusDenied)], [self failure:[MTRError errorForCHIPErrorCode:CHIP_ERROR_ACCESS_DENIED]]);
// If the whole log content fits into the response LogContent field or if there is no log, forward it to the caller
// and stop here.
if ([status isEqual:@(MTRDiagnosticLogsStatusExhausted)] || [status isEqual:@(MTRDiagnosticLogsStatusNoLogs)]) {
NSError * writeError = nil;
[self writeToFile:response.logContent error:&writeError];
VerifyOrReturn(nil == writeError, [self failure:writeError]);
[self success];
return;
};
// The file is going to be transferred over BDX. Everything past this point will be handled in bdxTransferDone.
}
- (void)createFile:(NSError **)error
{
VerifyOrReturn(nil == _fileHandle);
auto * fileManager = [NSFileManager defaultManager];
[fileManager URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:_fileURL
create:YES
error:error];
VerifyOrReturn(nil == *error);
BOOL success = [fileManager createFileAtPath:[_fileURL path] contents:nil attributes:nil];
VerifyOrReturn(success, *error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INTERNAL]);
auto * fileHandle = [NSFileHandle fileHandleForWritingToURL:_fileURL error:error];
VerifyOrReturn(nil == *error);
_fileHandle = fileHandle;
}
- (void)deleteFile
{
VerifyOrReturn(nil != _fileHandle);
NSError * error = nil;
[[NSFileManager defaultManager] removeItemAtPath:[_fileURL path] error:&error];
if (nil != error) {
// There is an error but there is really not much we can do at that point besides logging it.
MTR_LOG_ERROR("Error trying to delete the log file: %@. Error: %@", _fileURL, error);
}
}
- (void)writeToFile:(NSData *)data error:(out NSError **)error
{
[self createFile:error];
[_fileHandle seekToEndOfFile];
[_fileHandle writeData:data error:error];
}
- (BOOL)matches:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
{
return [_fileDesignator isEqualToString:fileDesignator] && [_fabricIndex isEqualToNumber:fabricIndex] && [_nodeID isEqualToNumber:nodeID];
}
- (void)failure:(NSError * _Nullable)error
{
_finalize(error);
}
- (void)success
{
_finalize(nil);
}
- (NSURL *)_toFileURL:(MTRDiagnosticLogType)type nodeID:(NSNumber *)nodeID
{
auto * dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.dateFormat = @"yyyy-MM-dd_HH:mm:ss.SSSZZZ";
auto * timeString = [dateFormatter stringFromDate:NSDate.now];
auto * nodeIDString = [self _toNodeIDString:nodeID];
auto * typeString = [self _toTypeString:type];
auto * filename = [NSString stringWithFormat:@"%@_%@_%@", timeString, nodeIDString, typeString];
return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:filename] isDirectory:YES];
}
- (NSString *)_toFileDesignatorString:(MTRDiagnosticLogType)type nodeID:(NSNumber *)nodeID
{
auto * nodeIDString = [self _toNodeIDString:nodeID];
auto * typeString = [self _toTypeString:type];
auto * fileDesignator = [NSString stringWithFormat:@"bdx://%@/%@", nodeIDString, typeString];
auto substringIndex = MIN(chip::bdx::DiagnosticLogs::kMaxFileDesignatorLen, [fileDesignator length]);
return [fileDesignator substringToIndex:substringIndex];
}
- (NSString *)_toNodeIDString:(NSNumber *)nodeID
{
return [NSString stringWithFormat:@"%016llX", nodeID.unsignedLongLongValue];
}
- (NSString *)_toTypeString:(MTRDiagnosticLogType)type
{
switch (type) {
case MTRDiagnosticLogTypeEndUserSupport:
return kEndUserSupport;
case MTRDiagnosticLogTypeNetworkDiagnostics:
return kNetworkDiagnostics;
case MTRDiagnosticLogTypeCrash:
return kCrash;
default:
// This should never happen.
chipDie();
}
}
@end
@implementation MTRDownloads
- (instancetype)init
{
if (self = [super init]) {
_downloads = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc
{
auto error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_INTERNAL];
for (MTRDownload * download in _downloads) {
[download failure:error];
}
_downloads = nil;
}
- (MTRDownload * _Nullable)get:(NSString *)fileDesignator fabricIndex:(NSNumber *)fabricIndex nodeID:(NSNumber *)nodeID
{
for (MTRDownload * download in _downloads) {
if ([download matches:fileDesignator fabricIndex:fabricIndex nodeID:nodeID]) {
return download;
}
}
return nil;
}
- (MTRDownload * _Nullable)add:(MTRDiagnosticLogType)type
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion
done:(void (^)(MTRDownload * finishedDownload))done
{
assertChipStackLockedByCurrentThread();
auto download = [[MTRDownload alloc] initWithType:type fabricIndex:fabricIndex nodeID:nodeID queue:queue completion:completion done:done];
VerifyOrReturnValue(nil != download, nil);
[_downloads addObject:download];
return download;
}
- (void)abortDownloadsForController:(MTRDeviceController_Concrete *)controller
{
assertChipStackLockedByCurrentThread();
auto fabricIndex = @(controller.fabricIndex);
for (MTRDownload * download in [_downloads copy]) {
if (![download.fabricIndex isEqual:fabricIndex]) {
continue;
}
[download failure:[MTRError errorForCHIPErrorCode:CHIP_ERROR_CANCELLED]];
[self remove:download];
}
}
- (void)remove:(MTRDownload *)download
{
assertChipStackLockedByCurrentThread();
[_downloads removeObject:download];
}
@end
@implementation MTRDiagnosticLogsDownloader
- (instancetype)init
{
assertChipStackLockedByCurrentThread();
if (self = [super init]) {
_downloads = [[MTRDownloads alloc] init];
_bridge = new DiagnosticLogsDownloaderBridge(self);
if (_bridge == nullptr) {
MTR_LOG_ERROR("Error: %@", kErrorInitDiagnosticLogsDownloader);
return nil;
}
}
return self;
}
- (void)dealloc
{
if (_bridge) {
delete _bridge;
_bridge = nil;
}
_downloads = nil;
}
- (chip::bdx::BDXTransferServerDelegate *)getBridge
{
return _bridge;
}
- (void)downloadLogFromNodeWithID:(NSNumber *)nodeID
controller:(MTRDeviceController_Concrete *)controller
type:(MTRDiagnosticLogType)type
timeout:(NSTimeInterval)timeout
queue:(dispatch_queue_t)queue
completion:(void (^)(NSURL * _Nullable url, NSError * _Nullable error))completion;
{
assertChipStackLockedByCurrentThread();
uint16_t timeoutInSeconds = 0;
if (timeout <= 0) {
timeoutInSeconds = 0;
} else if (timeout > UINT16_MAX) {
MTR_LOG("Warning: timeout is too large. It will be truncated to UINT16_MAX.");
timeoutInSeconds = UINT16_MAX;
} else {
timeoutInSeconds = static_cast<uint16_t>(timeout);
}
// This block is always called when a download is finished.
auto done = ^(MTRDownload * finishedDownload) {
[controller asyncDispatchToMatterQueue:^() {
[self->_downloads remove:finishedDownload];
if (timeoutInSeconds > 0) {
self->_bridge->CancelBDXTransferTimeout(finishedDownload);
}
} errorHandler:nil];
};
auto fabricIndex = @(controller.fabricIndex);
auto download = [_downloads add:type fabricIndex:fabricIndex nodeID:nodeID queue:queue completion:completion done:done];
VerifyOrReturn(nil != download,
dispatch_async(queue, ^{ completion(nil, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INTERNAL]); }));
auto interactionModelDone = ^(MTRDiagnosticLogsClusterRetrieveLogsResponseParams * _Nullable response, NSError * _Nullable error) {
[download checkInteractionModelResponse:response error:error];
};
auto * device = [MTRDevice deviceWithNodeID:nodeID controller:controller];
auto * cluster = [[MTRClusterDiagnosticLogs alloc] initWithDevice:device endpointID:@(kDiagnosticLogsEndPoint) queue:queue];
auto * params = [[MTRDiagnosticLogsClusterRetrieveLogsRequestParams alloc] init];
params.intent = @(type);
params.requestedProtocol = @(MTRDiagnosticLogsTransferProtocolBDX);
params.transferFileDesignator = download.fileDesignator;
[cluster retrieveLogsRequestWithParams:params expectedValues:nil expectedValueInterval:nil completion:interactionModelDone];
if (timeoutInSeconds > 0) {
auto err = _bridge->StartBDXTransferTimeout(download, timeoutInSeconds);
VerifyOrReturn(CHIP_NO_ERROR == err, [download failure:[MTRError errorForCHIPErrorCode:err]]);
}
}
- (void)abortDownloadsForController:(MTRDeviceController_Concrete *)controller;
{
assertChipStackLockedByCurrentThread();
[_downloads abortDownloadsForController:controller];
}
- (void)handleBDXTransferSessionBeginForFileDesignator:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
completion:(MTRStatusCompletion)completion
abortHandler:(AbortHandler)abortHandler;
{
assertChipStackLockedByCurrentThread();
MTR_LOG("BDX Transfer Session Begin for log download: %@", fileDesignator);
auto * download = [_downloads get:fileDesignator fabricIndex:fabricIndex nodeID:nodeID];
VerifyOrReturn(nil != download, completion([MTRError errorForCHIPErrorCode:CHIP_ERROR_NOT_FOUND]));
download.abortHandler = abortHandler;
completion(nil);
}
- (void)handleBDXTransferSessionDataForFileDesignator:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
data:(NSData *)data
completion:(MTRStatusCompletion)completion
{
assertChipStackLockedByCurrentThread();
MTR_LOG("BDX Transfer Session Data for log download: %@: %@", fileDesignator, data);
auto * download = [_downloads get:fileDesignator fabricIndex:fabricIndex nodeID:nodeID];
VerifyOrReturn(nil != download, completion([MTRError errorForCHIPErrorCode:CHIP_ERROR_NOT_FOUND]));
NSError * error = nil;
[download writeToFile:data error:&error];
VerifyOrReturn(nil != error, completion(error));
completion(nil);
}
- (void)handleBDXTransferSessionEndForFileDesignator:(NSString *)fileDesignator
fabricIndex:(NSNumber *)fabricIndex
nodeID:(NSNumber *)nodeID
error:(NSError * _Nullable)error
{
assertChipStackLockedByCurrentThread();
MTR_LOG("BDX Transfer Session End for log download: %@: %@", fileDesignator, error);
auto * download = [_downloads get:fileDesignator fabricIndex:fabricIndex nodeID:nodeID];
VerifyOrReturn(nil != download);
VerifyOrReturn(nil == error, [download failure:error]);
[download success];
}
@end
DiagnosticLogsDownloaderBridge::DiagnosticLogsDownloaderBridge(MTRDiagnosticLogsDownloader * delegate)
{
mDelegate = delegate;
}
DiagnosticLogsDownloaderBridge::~DiagnosticLogsDownloaderBridge()
{
mDelegate = nil;
}
CHIP_ERROR DiagnosticLogsDownloaderBridge::OnTransferBegin(chip::bdx::BDXTransferProxy * transfer)
{
VerifyOrReturnError(nil != mDelegate, CHIP_ERROR_INCORRECT_STATE);
auto fileDesignatorSpan = transfer->GetFileDesignator();
auto fileDesignator = AsString(fileDesignatorSpan);
VerifyOrReturnError(nil != fileDesignator, CHIP_ERROR_INCORRECT_STATE);
auto * fabricIndex = @(transfer->GetFabricIndex());
auto * nodeId = @(transfer->GetPeerNodeId());
auto completionHandler = ^(NSError * _Nullable error) {
assertChipStackLockedByCurrentThread();
if (error != nil) {
auto err = [MTRError errorToCHIPErrorCode:error];
transfer->Reject(err);
} else {
transfer->Accept();
}
};
auto abortHandler = ^(NSError * error) {
assertChipStackLockedByCurrentThread();
auto err = [MTRError errorToCHIPErrorCode:error];
transfer->Reject(err);
};
[mDelegate handleBDXTransferSessionBeginForFileDesignator:fileDesignator
fabricIndex:fabricIndex
nodeID:nodeId
completion:completionHandler
abortHandler:abortHandler];
return CHIP_NO_ERROR;
}
CHIP_ERROR DiagnosticLogsDownloaderBridge::OnTransferEnd(chip::bdx::BDXTransferProxy * transfer, CHIP_ERROR error)
{
VerifyOrReturnError(nil != mDelegate, CHIP_ERROR_INCORRECT_STATE);
auto fileDesignatorSpan = transfer->GetFileDesignator();
auto fileDesignator = AsString(fileDesignatorSpan);
VerifyOrReturnError(nil != fileDesignator, CHIP_ERROR_INCORRECT_STATE);
NSError * mtrError = nil;
if (CHIP_NO_ERROR != error) {
mtrError = [MTRError errorForCHIPErrorCode:error];
}
auto * fabricIndex = @(transfer->GetFabricIndex());
auto * nodeId = @(transfer->GetPeerNodeId());
[mDelegate handleBDXTransferSessionEndForFileDesignator:fileDesignator
fabricIndex:fabricIndex
nodeID:nodeId
error:mtrError];
return CHIP_NO_ERROR;
}
CHIP_ERROR DiagnosticLogsDownloaderBridge::OnTransferData(chip::bdx::BDXTransferProxy * transfer, const chip::ByteSpan & dataSpan)
{
VerifyOrReturnError(nil != mDelegate, CHIP_ERROR_INCORRECT_STATE);
auto fileDesignatorSpan = transfer->GetFileDesignator();
auto fileDesignator = AsString(fileDesignatorSpan);
VerifyOrReturnError(nil != fileDesignator, CHIP_ERROR_INCORRECT_STATE);
auto * fabricIndex = @(transfer->GetFabricIndex());
auto * nodeId = @(transfer->GetPeerNodeId());
auto data = AsData(dataSpan);
VerifyOrReturnError(nil != data, CHIP_ERROR_INCORRECT_STATE);
auto completionHandler = ^(NSError * _Nullable error) {
assertChipStackLockedByCurrentThread();
if (error != nil) {
auto err = [MTRError errorToCHIPErrorCode:error];
transfer->Reject(err);
} else {
transfer->Continue();
}
};
[mDelegate handleBDXTransferSessionDataForFileDesignator:fileDesignator
fabricIndex:fabricIndex
nodeID:nodeId
data:data
completion:completionHandler];
return CHIP_NO_ERROR;
}
CHIP_ERROR DiagnosticLogsDownloaderBridge::StartBDXTransferTimeout(MTRDownload * download, uint16_t timeoutInSeconds)
{
assertChipStackLockedByCurrentThread();
return chip::DeviceLayer::SystemLayer().StartTimer(chip::System::Clock::Seconds16(timeoutInSeconds), OnTransferTimeout, (__bridge void *) download);
}
void DiagnosticLogsDownloaderBridge::CancelBDXTransferTimeout(MTRDownload * download)
{
assertChipStackLockedByCurrentThread();
chip::DeviceLayer::SystemLayer().CancelTimer(OnTransferTimeout, (__bridge void *) download);
}
void DiagnosticLogsDownloaderBridge::OnTransferTimeout(chip::System::Layer * layer, void * context)
{
assertChipStackLockedByCurrentThread();
auto * download = (__bridge MTRDownload *) context;
VerifyOrReturn(nil != download);
// If there is no abortHandler, it means that the BDX transfer has not started.
// When a BDX transfer has started we need to abort the transfer and we would error out
// at next poll. We would end up calling OnTransferEnd and eventually [download failure:error].
// But if the transfer has not started we would stop right now.
auto error = [MTRError errorForCHIPErrorCode:CHIP_ERROR_TIMEOUT];
if (download.abortHandler == nil) {
[download failure:error];
} else {
download.abortHandler(error);
}
}