blob: efa84ef9168a8efdcd8b2b19653af2a017a7f849 [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 "MTROTAImageTransferHandler.h"
#import "MTRDeviceControllerFactory_Internal.h"
#import "MTRDeviceController_Internal.h"
#import "MTRError_Internal.h"
#import "MTROTAUnsolicitedBDXMessageHandler.h"
#import "NSStringSpanConversion.h"
using namespace chip;
using namespace chip::bdx;
using namespace chip::app;
constexpr uint32_t kMaxBdxBlockSize = 1024;
// Timeout for the BDX transfer session. The OTA Spec mandates this should be >= 5 minutes.
constexpr System::Clock::Timeout kBdxTimeout = System::Clock::Seconds16(5 * 60);
constexpr bdx::TransferRole kBdxRole = bdx::TransferRole::kSender;
// An ARC-managed object that lets us do weak references to a MTROTAImageTransferHandler
// (which is a C++ object).
@interface MTROTAImageTransferHandlerWrapper : NSObject
- (instancetype)initWithMTROTAImageTransferHandler:(MTROTAImageTransferHandler *)otaImageTransferHandler;
// Non-atomic property since it's read or written only on the Matter queue.
@property (nonatomic, nullable, readwrite, assign) MTROTAImageTransferHandler * otaImageTransferHandler;
@end
@implementation MTROTAImageTransferHandlerWrapper
- (instancetype)initWithMTROTAImageTransferHandler:(MTROTAImageTransferHandler *)otaImageTransferHandler
{
if (self = [super init]) {
_otaImageTransferHandler = otaImageTransferHandler;
}
return self;
}
@end
MTROTAImageTransferHandler::MTROTAImageTransferHandler(chip::System::Layer * layer)
{
assertChipStackLockedByCurrentThread();
mSystemLayer = layer;
MTROTAUnsolicitedBDXMessageHandler::GetInstance()->OnTransferHandlerCreated(this);
mOTAImageTransferHandlerWrapper = [[MTROTAImageTransferHandlerWrapper alloc] initWithMTROTAImageTransferHandler:this];
}
CHIP_ERROR MTROTAImageTransferHandler::Init(Messaging::ExchangeContext * exchangeCtx)
{
assertChipStackLockedByCurrentThread();
mPeer = GetPeerScopedNodeId(exchangeCtx);
auto * controller = [[MTRDeviceControllerFactory sharedInstance] runningControllerForFabricIndex:mPeer.GetFabricIndex()];
VerifyOrReturnError(controller != nil, CHIP_ERROR_INCORRECT_STATE);
mDelegate = controller.otaProviderDelegate;
mDelegateNotificationQueue = controller.otaProviderDelegateQueue;
// We should have already checked that this controller supports OTA.
VerifyOrReturnError(mDelegate != nil, CHIP_ERROR_INCORRECT_STATE);
VerifyOrReturnError(mDelegateNotificationQueue != nil, CHIP_ERROR_INCORRECT_STATE);
BitFlags<bdx::TransferControlFlags> flags(bdx::TransferControlFlags::kReceiverDrive);
return AsyncResponder::Init(mSystemLayer, exchangeCtx, kBdxRole, flags, kMaxBdxBlockSize, kBdxTimeout);
}
MTROTAImageTransferHandler::~MTROTAImageTransferHandler()
{
assertChipStackLockedByCurrentThread();
if (mNeedToCallTransferSessionEnd) {
// TODO: Store the actual error involved in error cases, so we can pass the right thing here.
InvokeTransferSessionEndCallback(CHIP_ERROR_INTERNAL);
}
MTROTAUnsolicitedBDXMessageHandler::GetInstance()->OnTransferHandlerDestroyed(this);
mOTAImageTransferHandlerWrapper.otaImageTransferHandler = nullptr;
}
CHIP_ERROR MTROTAImageTransferHandler::OnTransferSessionBegin(const TransferSession::OutputEventType eventType)
{
assertChipStackLockedByCurrentThread();
uint16_t fdl = 0;
auto fd = mTransfer.GetFileDesignator(fdl);
VerifyOrReturnError(fdl <= bdx::kMaxFileDesignatorLen, CHIP_ERROR_INCORRECT_STATE);
CharSpan fileDesignatorSpan(Uint8::to_const_char(fd), fdl);
auto fileDesignator = AsString(fileDesignatorSpan);
if (fileDesignator == nil) {
return CHIP_ERROR_INCORRECT_STATE;
}
auto offset = @(mTransfer.GetStartOffset());
auto * controller = [[MTRDeviceControllerFactory sharedInstance] runningControllerForFabricIndex:mPeer.GetFabricIndex()];
VerifyOrReturnError(controller != nil, CHIP_ERROR_INCORRECT_STATE);
MTROTAImageTransferHandlerWrapper * __weak weakWrapper = mOTAImageTransferHandlerWrapper;
auto completionHandler = ^(NSError * _Nullable error) {
[controller
asyncDispatchToMatterQueue:^() {
assertChipStackLockedByCurrentThread();
// Check if the OTA image transfer handler is still valid. If not, return from the completion handler.
MTROTAImageTransferHandlerWrapper * strongWrapper = weakWrapper;
if (!strongWrapper || !strongWrapper.otaImageTransferHandler) {
return;
}
if (error != nil) {
CHIP_ERROR err = [MTRError errorToCHIPErrorCode:error];
LogErrorOnFailure(err);
NotifyEventHandled(eventType, err);
return;
}
// bdx::TransferSession will automatically reject a transfer if there are no
// common supported control modes. It will also default to the smaller
// block size.
TransferSession::TransferAcceptData acceptData;
acceptData.ControlMode = bdx::TransferControlFlags::kReceiverDrive;
acceptData.MaxBlockSize = mTransfer.GetTransferBlockSize();
acceptData.StartOffset = mTransfer.GetStartOffset();
acceptData.Length = mTransfer.GetTransferLength();
CHIP_ERROR err = mTransfer.AcceptTransfer(acceptData);
LogErrorOnFailure(err);
NotifyEventHandled(eventType, err);
}
errorHandler:^(NSError *) {
// Not much we can do here, but if the controller is shut down we
// should already have been destroyed anyway.
}];
};
auto nodeId = @(mPeer.GetNodeId());
auto strongDelegate = mDelegate;
auto delegateQueue = mDelegateNotificationQueue;
if (strongDelegate == nil || delegateQueue == nil) {
LogErrorOnFailure(CHIP_ERROR_INCORRECT_STATE);
return CHIP_ERROR_INCORRECT_STATE;
}
dispatch_async(delegateQueue, ^{
if ([strongDelegate respondsToSelector:@selector
(handleBDXTransferSessionBeginForNodeID:controller:fileDesignator:offset:completion:)]) {
[strongDelegate handleBDXTransferSessionBeginForNodeID:nodeId
controller:controller
fileDesignator:fileDesignator
offset:offset
completion:completionHandler];
} else {
[strongDelegate handleBDXTransferSessionBeginForNodeID:nodeId
controller:controller
fileDesignator:fileDesignator
offset:offset
completionHandler:completionHandler];
}
});
mNeedToCallTransferSessionEnd = true;
return CHIP_NO_ERROR;
}
void MTROTAImageTransferHandler::InvokeTransferSessionEndCallback(CHIP_ERROR error)
{
assertChipStackLockedByCurrentThread();
mNeedToCallTransferSessionEnd = false;
auto * controller = [[MTRDeviceControllerFactory sharedInstance] runningControllerForFabricIndex:mPeer.GetFabricIndex()];
VerifyOrReturn(controller != nil);
auto nodeId = @(mPeer.GetNodeId());
auto strongDelegate = mDelegate;
auto delegateQueue = mDelegateNotificationQueue;
if (strongDelegate == nil || delegateQueue == nil) {
LogErrorOnFailure(CHIP_ERROR_INCORRECT_STATE);
return;
}
auto nsError = [MTRError errorForCHIPErrorCode:error];
if ([strongDelegate respondsToSelector:@selector(handleBDXTransferSessionEndForNodeID:controller:error:)]) {
dispatch_async(delegateQueue, ^{
[strongDelegate handleBDXTransferSessionEndForNodeID:nodeId
controller:controller
error:nsError];
});
}
}
CHIP_ERROR MTROTAImageTransferHandler::OnTransferSessionEnd(const TransferSession::OutputEventType eventType)
{
assertChipStackLockedByCurrentThread();
CHIP_ERROR error = CHIP_NO_ERROR;
if (eventType == TransferSession::OutputEventType::kTransferTimeout) {
error = CHIP_ERROR_TIMEOUT;
} else if (eventType != TransferSession::OutputEventType::kAckEOFReceived) {
error = CHIP_ERROR_INTERNAL;
}
InvokeTransferSessionEndCallback(error);
if (error == CHIP_NO_ERROR) {
NotifyEventHandled(eventType, error);
}
return error;
}
CHIP_ERROR MTROTAImageTransferHandler::OnBlockQuery(const TransferSession::OutputEventType eventType, uint64_t bytesToSkip)
{
assertChipStackLockedByCurrentThread();
auto blockSize = @(mTransfer.GetTransferBlockSize());
auto blockIndex = @(mTransfer.GetNextBlockNum());
auto * controller = [[MTRDeviceControllerFactory sharedInstance] runningControllerForFabricIndex:mPeer.GetFabricIndex()];
VerifyOrReturnError(controller != nil, CHIP_ERROR_INCORRECT_STATE);
MTROTAImageTransferHandlerWrapper * __weak weakWrapper = mOTAImageTransferHandlerWrapper;
auto completionHandler = ^(NSData * _Nullable data, BOOL isEOF) {
[controller
asyncDispatchToMatterQueue:^() {
assertChipStackLockedByCurrentThread();
// Check if the OTA image transfer handler is still valid. If not, return from the completion handler.
MTROTAImageTransferHandlerWrapper * strongWrapper = weakWrapper;
if (!strongWrapper || !strongWrapper.otaImageTransferHandler) {
return;
}
if (data == nil) {
NotifyEventHandled(eventType, CHIP_ERROR_INCORRECT_STATE);
return;
}
TransferSession::BlockData blockData;
blockData.Data = static_cast<const uint8_t *>([data bytes]);
blockData.Length = static_cast<size_t>([data length]);
blockData.IsEof = isEOF;
CHIP_ERROR err = mTransfer.PrepareBlock(blockData);
LogErrorOnFailure(err);
NotifyEventHandled(eventType, err);
}
errorHandler:^(NSError *) {
// Not much we can do here, but if the controller is shut down we
// should already have been destroyed anyway.
}];
};
// TODO Handle MaxLength
auto nodeId = @(mPeer.GetNodeId());
auto strongDelegate = mDelegate;
auto delegateQueue = mDelegateNotificationQueue;
if (strongDelegate == nil || delegateQueue == nil) {
LogErrorOnFailure(CHIP_ERROR_INCORRECT_STATE);
return CHIP_ERROR_INCORRECT_STATE;
}
dispatch_async(delegateQueue, ^{
if ([strongDelegate respondsToSelector:@selector(handleBDXQueryForNodeID:
controller:blockSize:blockIndex:bytesToSkip:completion:)]) {
[strongDelegate handleBDXQueryForNodeID:nodeId
controller:controller
blockSize:blockSize
blockIndex:blockIndex
bytesToSkip:@(bytesToSkip)
completion:completionHandler];
} else {
[strongDelegate handleBDXQueryForNodeID:nodeId
controller:controller
blockSize:blockSize
blockIndex:blockIndex
bytesToSkip:@(bytesToSkip)
completionHandler:completionHandler];
}
});
return CHIP_NO_ERROR;
}
void MTROTAImageTransferHandler::HandleTransferSessionOutput(TransferSession::OutputEvent & event)
{
VerifyOrReturn(mDelegate != nil);
TransferSession::OutputEventType eventType = event.EventType;
ChipLogError(BDX, "OutputEvent type: %s", event.ToString(eventType));
CHIP_ERROR err = CHIP_NO_ERROR;
switch (event.EventType) {
case TransferSession::OutputEventType::kInitReceived:
err = OnTransferSessionBegin(eventType);
if (err != CHIP_NO_ERROR) {
LogErrorOnFailure(err);
NotifyEventHandled(eventType, err);
}
break;
case TransferSession::OutputEventType::kStatusReceived:
ChipLogError(BDX, "Got StatusReport %x", static_cast<uint16_t>(event.statusData.statusCode));
[[fallthrough]];
case TransferSession::OutputEventType::kAckEOFReceived:
case TransferSession::OutputEventType::kInternalError:
case TransferSession::OutputEventType::kTransferTimeout:
err = OnTransferSessionEnd(eventType);
if (err != CHIP_NO_ERROR) {
LogErrorOnFailure(err);
NotifyEventHandled(eventType, err);
}
break;
case TransferSession::OutputEventType::kQueryWithSkipReceived:
case TransferSession::OutputEventType::kQueryReceived: {
uint64_t bytesToSkip = (eventType == TransferSession::OutputEventType::kQueryWithSkipReceived ? event.bytesToSkip.BytesToSkip : 0);
err = OnBlockQuery(eventType, bytesToSkip);
if (err != CHIP_NO_ERROR) {
LogErrorOnFailure(err);
NotifyEventHandled(eventType, err);
}
break;
}
case TransferSession::OutputEventType::kNone:
case TransferSession::OutputEventType::kAckReceived:
case TransferSession::OutputEventType::kAcceptReceived:
case TransferSession::OutputEventType::kBlockReceived:
default:
// Should never happen since this implements an OTA provider in BDX Receiver role.
chipDie();
break;
}
}
void MTROTAImageTransferHandler::DestroySelf()
{
assertChipStackLockedByCurrentThread();
delete this;
}
ScopedNodeId MTROTAImageTransferHandler::GetPeerScopedNodeId(Messaging::ExchangeContext * ec)
{
auto sessionHandle = ec->GetSessionHandle();
if (sessionHandle->IsSecureSession()) {
return sessionHandle->AsSecureSession()->GetPeer();
} else if (sessionHandle->IsGroupSession()) {
return sessionHandle->AsIncomingGroupSession()->GetPeer();
}
return ScopedNodeId();
}
CHIP_ERROR MTROTAImageTransferHandler::OnMessageReceived(
Messaging::ExchangeContext * ec, const PayloadHeader & payloadHeader, System::PacketBufferHandle && payload)
{
assertChipStackLockedByCurrentThread();
ChipLogProgress(BDX, "MTROTAImageTransferHandler: OnMessageReceived: message " ChipLogFormatMessageType " protocol " ChipLogFormatProtocolId,
payloadHeader.GetMessageType(), ChipLogValueProtocolId(payloadHeader.GetProtocolID()));
VerifyOrReturnError(ec != nullptr, CHIP_ERROR_INCORRECT_STATE);
CHIP_ERROR err;
// If we receive a ReceiveInit message, then we prepare for transfer.
//
// If init succeeds, or is not needed, we send the message to the AsyncTransferFacilitator for processing.
if (payloadHeader.HasMessageType(MessageType::ReceiveInit)) {
err = Init(ec);
if (err != CHIP_NO_ERROR) {
ChipLogError(Controller, "OnMessageReceived: Failed to prepare for transfer for BDX: %" CHIP_ERROR_FORMAT, err.Format());
return err;
}
}
// Send the message to the AsyncFacilitator to drive the BDX session state machine.
AsyncTransferFacilitator::OnMessageReceived(ec, payloadHeader, std::move(payload));
return err;
}