| /* |
| * |
| * Copyright (c) 2021 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 <Foundation/Foundation.h> |
| |
| #import "MTRBaseDevice_Internal.h" |
| #import "MTRDeviceController_Concrete.h" |
| #import "MTRDeviceController_Internal.h" |
| #import "MTRError_Internal.h" |
| #import "MTRLogging_Internal.h" |
| #import "zap-generated/MTRBaseClusters.h" |
| |
| #include <app/data-model/NullObject.h> |
| #include <messaging/ExchangeMgr.h> |
| #include <platform/CHIPDeviceLayer.h> |
| #include <transport/Session.h> |
| |
| NS_ASSUME_NONNULL_BEGIN |
| |
| /** |
| * Bridge that allows invoking a given MTRActionBlock on the Matter queue, after |
| * communication with the device in question has been established, as far as we |
| * know. |
| */ |
| class MTRCallbackBridgeBase { |
| public: |
| /** |
| * Run the given MTRActionBlock on the Matter thread after getting a secure |
| * session corresponding to the given MTRBaseDevice. On success, convert |
| * the success value to whatever type it needs to be to call the callback |
| * type we're templated over. Once this function has been called, on a callback |
| * bridge allocated with `new`, the bridge object must not be accessed by |
| * the caller. The action block will handle deleting the bridge. |
| */ |
| void DispatchAction(MTRBaseDevice * device) && |
| { |
| if (device.isPASEDevice) { |
| ActionWithPASEDevice(device); |
| } else { |
| ActionWithNodeID(device.nodeID, device.concreteController); |
| } |
| } |
| |
| protected: |
| MTRCallbackBridgeBase(dispatch_queue_t queue) |
| : mQueue(queue) |
| { |
| } |
| |
| virtual ~MTRCallbackBridgeBase() {}; |
| |
| virtual void MaybeDoAction(chip::Messaging::ExchangeManager * _Nullable exchangeManager, |
| const chip::Optional<chip::SessionHandle> & session, NSError * _Nullable error) |
| = 0; |
| virtual void LogRequestStart() = 0; |
| |
| void ActionWithPASEDevice(MTRBaseDevice * device) |
| { |
| LogRequestStart(); |
| |
| MTRDeviceController_Concrete * _Nullable controller = device.concreteController; |
| if (!controller) { |
| MTR_LOG_ERROR("Trying to perform action with PASE device with a non-concrete controller"); |
| MaybeDoAction(nullptr, chip::NullOptional, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]); |
| return; |
| } |
| |
| [controller getSessionForCommissioneeDevice:device.nodeID |
| completion:^(chip::Messaging::ExchangeManager * exchangeManager, |
| const chip::Optional<chip::SessionHandle> & session, NSError * _Nullable error, NSNumber * _Nullable retryDelay) { |
| MaybeDoAction(exchangeManager, session, error); |
| }]; |
| } |
| |
| void ActionWithNodeID(chip::NodeId nodeID, MTRDeviceController_Concrete * _Nullable controller) |
| { |
| LogRequestStart(); |
| |
| if (!controller) { |
| MTR_LOG_ERROR("Trying to perform action on node ID 0x%016llX (%llu) with a non-concrete controller", nodeID, nodeID); |
| MaybeDoAction(nullptr, chip::NullOptional, [MTRError errorForCHIPErrorCode:CHIP_ERROR_INCORRECT_STATE]); |
| return; |
| } |
| |
| [controller getSessionForNode:nodeID |
| completion:^(chip::Messaging::ExchangeManager * _Nullable exchangeManager, |
| const chip::Optional<chip::SessionHandle> & session, NSError * _Nullable error, NSNumber * _Nullable retryDelay) { |
| MaybeDoAction(exchangeManager, session, error); |
| }]; |
| } |
| |
| // OnDone and KeepAliveOnCallback really only make sense for subscription |
| // bridges, but we put them here to avoid many copies of this code in |
| // generated bits. |
| void OnDone() |
| { |
| if (!mQueue) { |
| delete this; |
| return; |
| } |
| |
| // Delete ourselves async, so that any error/data reports we |
| // queued up before getting OnDone have a chance to run. |
| auto * self = this; |
| dispatch_async(mQueue, ^{ |
| delete self; |
| }); |
| } |
| |
| void KeepAliveOnCallback() { mKeepAlive = true; } |
| |
| dispatch_queue_t mQueue; |
| bool mKeepAlive = false; |
| }; |
| |
| typedef void (^MTRResponseHandler)(id _Nullable value, NSError * _Nullable error); |
| typedef void (*MTRErrorCallback)(void * context, CHIP_ERROR error); |
| |
| /** |
| * The bridge will pass itself as the last argument to the action block. |
| * |
| * The action block must do one of three things: |
| * |
| * 1) Return an error. |
| * 2) Call the "successCb" callback, with the bridge passed to the block as the |
| * context, possibly asynchronously. |
| * 3) Call the "failureCb" callback, with the bridge passed to the block as the |
| * context, possibly asynchronously. |
| * |
| * For an MTRCallbackBridge that has keepAlive set to true, the success/failure |
| * callbacks may be called multiple times. If keepAlive is false, there must be |
| * no calls after the first one. |
| */ |
| template <typename SuccessCallback> |
| using MTRActionBlockT = CHIP_ERROR (^)(chip::Messaging::ExchangeManager & exchangeManager, const chip::SessionHandle & session, |
| SuccessCallback successCb, MTRErrorCallback failureCb, MTRCallbackBridgeBase * bridge); |
| template <typename SuccessCallback> |
| using MTRLocalActionBlockT = CHIP_ERROR (^)(SuccessCallback successCb, MTRErrorCallback failureCb); |
| |
| template <class T> |
| class MTRCallbackBridge : public MTRCallbackBridgeBase { |
| public: |
| using MTRActionBlock = MTRActionBlockT<T>; |
| using MTRLocalActionBlock = MTRLocalActionBlockT<T>; |
| using SuccessCallbackType = T; |
| |
| /** |
| * Construct a callback bridge, which can then have DispatchAction() called |
| * on it. |
| */ |
| MTRCallbackBridge(dispatch_queue_t queue, MTRResponseHandler handler, MTRActionBlock _Nonnull action, T OnSuccessFn) |
| : MTRCallbackBridgeBase(queue) |
| , mHandler(handler) |
| , mAction(action) |
| , mSuccess(OnSuccessFn) |
| , mFailure(OnFailureFn) |
| { |
| } |
| |
| void LogRequestStart() override |
| { |
| mRequestTime = [NSDate date]; |
| // Generate a unique cookie to track this operation |
| mCookie = [NSString stringWithFormat:@"Response Time: %s+%u", typeid(T).name(), arc4random()]; |
| ChipLogDetail(Controller, "%s", mCookie.UTF8String); |
| } |
| |
| void MaybeDoAction(chip::Messaging::ExchangeManager * _Nullable exchangeManager, |
| const chip::Optional<chip::SessionHandle> & session, NSError * _Nullable error) override |
| { |
| // Make sure we don't hold on to our action longer than we have to. |
| auto action = mAction; |
| mAction = nil; |
| if (error != nil) { |
| DispatchFailure(this, error); |
| return; |
| } |
| |
| CHIP_ERROR err = action(*exchangeManager, session.Value(), mSuccess, mFailure, this); |
| if (err != CHIP_NO_ERROR) { |
| ChipLogError(Controller, "Failure performing action. C++-mangled success callback type: '%s', error: %s", |
| typeid(T).name(), chip::ErrorStr(err)); |
| |
| // Take the normal async error-reporting codepath. This will also |
| // handle cleaning us up properly. |
| OnFailureFn(this, err); |
| } |
| } |
| |
| virtual ~MTRCallbackBridge() {}; |
| |
| protected: |
| static void OnFailureFn(void * context, CHIP_ERROR error) { DispatchFailure(context, [MTRError errorForCHIPErrorCode:error]); } |
| |
| static void DispatchSuccess(void * context, id _Nullable value) { DispatchCallbackResult(context, nil, value); } |
| |
| static void DispatchFailure(void * context, NSError * error) { DispatchCallbackResult(context, error, nil); } |
| |
| private: |
| static void DispatchCallbackResult(void * context, NSError * _Nullable error, id _Nullable value) |
| { |
| MTRCallbackBridge * callbackBridge = static_cast<MTRCallbackBridge *>(context); |
| if (!callbackBridge) { |
| return; |
| } |
| |
| if (!callbackBridge->mQueue) { |
| ChipLogDetail(Controller, "%s %f seconds: can't dispatch response; no queue", callbackBridge->mCookie.UTF8String, |
| -[callbackBridge->mRequestTime timeIntervalSinceNow]); |
| if (!callbackBridge->mKeepAlive) { |
| delete callbackBridge; |
| } |
| return; |
| } |
| |
| dispatch_async(callbackBridge->mQueue, ^{ |
| ChipLogDetail(Controller, "%s %f seconds", callbackBridge->mCookie.UTF8String, |
| -[callbackBridge->mRequestTime timeIntervalSinceNow]); |
| callbackBridge->mHandler(value, error); |
| |
| if (!callbackBridge->mKeepAlive) { |
| delete callbackBridge; |
| } |
| }); |
| } |
| |
| MTRResponseHandler mHandler; |
| MTRActionBlock _Nullable mAction; |
| // Keep our subclasses from accessing mKeepAlive directly, by putting this |
| // "using" in our private section. |
| using MTRCallbackBridgeBase::mKeepAlive; |
| |
| T mSuccess; |
| MTRErrorCallback mFailure; |
| |
| // Measure the time it took for the callback to trigger |
| NSDate * mRequestTime; |
| NSString * mCookie; |
| }; |
| |
| NS_ASSUME_NONNULL_END |