Skip to content

Commit

Permalink
Introduce NativeDebuggerSessionObserver module (facebook#45577)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#45577

# Changelog: [Internal]

This diff adds new native module, which can be used from JavaScript.

The API includes:
1. `hasActiveSession`: returns a boolean flag, which can be used for determining if 1 or more debugging sessions are active for current HostTarget.
2. `subscribe`: receives a callback, which will be executed once the debugging state changes. To be more precise, this will only be called when state is changing from no active sessions to 1 session or the other way around. Callback should expect to receive one boolean argument, which can be used for determining  if there is an active session.

Reviewed By: huntie

Differential Revision: D59975264

fbshipit-source-id: dd095954529f573f38e9fae1792465a59e639d23
  • Loading branch information
hoxyq authored and facebook-github-bot committed Jul 25, 2024
1 parent 8af5e89 commit 6fde836
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "HostTarget.h"
#include "CdpJson.h"
#include "HostAgent.h"
#include "HostTargetSessionObserver.h"
#include "InspectorInterfaces.h"
#include "InspectorUtilities.h"
#include "InstanceTarget.h"
Expand Down Expand Up @@ -43,7 +44,13 @@ class HostTargetSession {
targetController,
std::move(hostMetadata),
state_,
executor) {}
executor) {
HostTargetSessionObserver::getInstance().onHostTargetSessionCreated();
}

~HostTargetSession() {
HostTargetSessionObserver::getInstance().onHostTargetSessionDestroyed();
}

/**
* Called by CallbackLocalConnection to send a message to this Session's
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <cassert>
#include <memory>
#include <mutex>

#include "HostTargetSessionObserver.h"

namespace facebook::react::jsinspector_modern {

HostTargetSessionObserver& HostTargetSessionObserver::getInstance() {
static HostTargetSessionObserver instance;
return instance;
}

// Will be called by HostTargetSession on inspector thread.
void HostTargetSessionObserver::onHostTargetSessionCreated() {
std::lock_guard<std::mutex> lock(mutex_);

++activeSessionCount_;
if (activeSessionCount_ == 1) {
for (auto& subscriber : subscribers_) {
subscriber.second(true);
}
}
}

// Will be called by HostTargetSession on inspector thread.
void HostTargetSessionObserver::onHostTargetSessionDestroyed() {
std::lock_guard<std::mutex> lock(mutex_);

assert(
activeSessionCount_ > 0 && "Unexpected overflow of HostTarget sessions");
--activeSessionCount_;
if (activeSessionCount_ == 0) {
for (auto& subscriber : subscribers_) {
subscriber.second(false);
}
}
}

// Will be called by NativeDebuggerSessionObserver on JS thread.
bool HostTargetSessionObserver::hasActiveSessions() {
std::lock_guard<std::mutex> lock(mutex_);

return activeSessionCount_ > 0;
}

// Will be called by NativeDebuggerSessionObserver on JS thread.
std::function<void()> HostTargetSessionObserver::subscribe(
std::function<void(bool)> callback) {
std::lock_guard<std::mutex> lock(mutex_);

auto subscriberIndex = subscriberIndex_++;
subscribers_.emplace(subscriberIndex, std::move(callback));

// Since HostTargetSessionObserver is a singleton, it is expected to outlive
// all potential subscribers
return [this, subscriberIndexToRemove = subscriberIndex]() {
std::lock_guard<std::mutex> lock(mutex_);
subscribers_.erase(subscriberIndexToRemove);
};
}

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <cstdint>
#include <functional>
#include <map>
#include <mutex>

namespace facebook::react::jsinspector_modern {

class HostTargetSessionObserver {
public:
static HostTargetSessionObserver& getInstance();

/*
* Not copyable.
*/
HostTargetSessionObserver(const HostTargetSessionObserver&) = delete;
HostTargetSessionObserver& operator=(const HostTargetSessionObserver&) =
delete;

/*
* Not movable.
*/
HostTargetSessionObserver(HostTargetSessionObserver&&) = delete;
HostTargetSessionObserver& operator=(HostTargetSessionObserver&&) = delete;

void onHostTargetSessionCreated();
void onHostTargetSessionDestroyed();

bool hasActiveSessions();
std::function<void()> subscribe(std::function<void(bool)> callback);

private:
HostTargetSessionObserver() = default;
~HostTargetSessionObserver() = default;

int activeSessionCount_ = 0;
std::map<uint32_t, std::function<void(bool)>> subscribers_;
uint32_t subscriberIndex_ = 0;

std::mutex mutex_;
};

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include <folly/executors/QueuedImmediateExecutor.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <jsinspector-modern/HostTarget.h>
#include <jsinspector-modern/HostTargetSessionObserver.h>
#include <jsinspector-modern/InspectorInterfaces.h>

#include <memory>

#include "InspectorMocks.h"
#include "UniquePtrFactory.h"

using namespace ::testing;

namespace facebook::react::jsinspector_modern {

namespace {

class HostTargetSessionObserverTest : public Test {
folly::QueuedImmediateExecutor immediateExecutor_;

protected:
HostTargetSessionObserverTest() {
EXPECT_CALL(runtimeTargetDelegate_, createAgentDelegate(_, _, _, _, _))
.WillRepeatedly(runtimeAgentDelegates_.lazily_make_unique<
FrontendChannel,
SessionState&,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&,
RuntimeExecutor>());
}

void connect() {
auto connection = makeConnection();

pageConnectionsPointers_.push_back(std::move(connection.first));
}

std::pair<std::unique_ptr<ILocalConnection>, MockRemoteConnection&>
makeConnection() {
size_t connectionIndex = remoteConnections_.objectsVended();
auto toPage = page_->connect(remoteConnections_.make_unique());

// We'll always get an onDisconnect call when we tear
// down the test. Expect it in order to satisfy the strict mock.
EXPECT_CALL(*remoteConnections_[connectionIndex], onDisconnect());
return {std::move(toPage), *remoteConnections_[connectionIndex]};
}

MockHostTargetDelegate hostTargetDelegate_;

VoidExecutor inspectorExecutor_ = [this](auto callback) {
immediateExecutor_.add(callback);
};

std::shared_ptr<HostTarget> page_ =
HostTarget::create(hostTargetDelegate_, inspectorExecutor_);

MockRuntimeTargetDelegate runtimeTargetDelegate_;

UniquePtrFactory<StrictMock<MockRuntimeAgentDelegate>> runtimeAgentDelegates_;

MOCK_METHOD(void, subscriptionCallback, (bool hasActiveSession));

private:
UniquePtrFactory<StrictMock<MockRemoteConnection>> remoteConnections_;

protected:
std::vector<std::unique_ptr<ILocalConnection>> pageConnectionsPointers_;
};
} // namespace

TEST_F(HostTargetSessionObserverTest, HasNoActiveSessionsByDefault) {
EXPECT_FALSE(HostTargetSessionObserver::getInstance().hasActiveSessions());
}

TEST_F(HostTargetSessionObserverTest, HasActiveSessionOnceConnected) {
connect();
EXPECT_TRUE(HostTargetSessionObserver::getInstance().hasActiveSessions());
}

TEST_F(HostTargetSessionObserverTest, HasNoActiveSessionsOnceDisconnected) {
connect();
EXPECT_TRUE(HostTargetSessionObserver::getInstance().hasActiveSessions());

pageConnectionsPointers_[0]->disconnect();
EXPECT_FALSE(HostTargetSessionObserver::getInstance().hasActiveSessions());
}

TEST_F(HostTargetSessionObserverTest, WorksWithMultipleConnections) {
connect();
EXPECT_TRUE(HostTargetSessionObserver::getInstance().hasActiveSessions());

connect();
EXPECT_TRUE(HostTargetSessionObserver::getInstance().hasActiveSessions());

pageConnectionsPointers_[0]->disconnect();
EXPECT_TRUE(HostTargetSessionObserver::getInstance().hasActiveSessions());

pageConnectionsPointers_[1]->disconnect();
EXPECT_FALSE(HostTargetSessionObserver::getInstance().hasActiveSessions());
}

TEST_F(HostTargetSessionObserverTest, CorrectlyNotifiesSubscribers) {
auto callback = [&](bool hasActiveSession) {
this->subscriptionCallback(hasActiveSession);
};
auto unsubscribe =
HostTargetSessionObserver::getInstance().subscribe(callback);

EXPECT_CALL(*this, subscriptionCallback(true)).Times(1);
connect();
connect();

pageConnectionsPointers_[0]->disconnect();
EXPECT_CALL(*this, subscriptionCallback(false)).Times(1);
pageConnectionsPointers_[1]->disconnect();
}

TEST_F(HostTargetSessionObserverTest, SupportsUnsubscribing) {
auto callback = [&](bool hasActiveSession) {
this->subscriptionCallback(hasActiveSession);
};
auto unsubscribe =
HostTargetSessionObserver::getInstance().subscribe(callback);

EXPECT_CALL(*this, subscriptionCallback(true)).Times(1);
connect();
connect();

unsubscribe();

EXPECT_CALL(*this, subscriptionCallback(false)).Times(0);
pageConnectionsPointers_[0]->disconnect();
pageConnectionsPointers_[1]->disconnect();
}

} // namespace facebook::react::jsinspector_modern
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include "NativeDebuggerSessionObserver.h"
#include <jsinspector-modern/HostTargetSessionObserver.h>

#include "Plugins.h"

std::shared_ptr<facebook::react::TurboModule>
NativeDebuggerSessionObserverModuleProvider(
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
return std::make_shared<facebook::react::NativeDebuggerSessionObserver>(
std::move(jsInvoker));
}

namespace facebook::react {

NativeDebuggerSessionObserver::NativeDebuggerSessionObserver(
std::shared_ptr<CallInvoker> jsInvoker)
: NativeDebuggerSessionObserverCxxSpec(std::move(jsInvoker)) {}

bool NativeDebuggerSessionObserver::hasActiveSession(
jsi::Runtime& /*runtime*/) {
return jsinspector_modern::HostTargetSessionObserver::getInstance()
.hasActiveSessions();
}

std::function<void()> NativeDebuggerSessionObserver::subscribe(
jsi::Runtime& /*runtime*/,
AsyncCallback<bool> callback) {
return jsinspector_modern::HostTargetSessionObserver::getInstance().subscribe(
[callback = std::move(callback)](bool sessionStatus) {
callback(sessionStatus);
});
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#pragma once

#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>

namespace facebook::react {

class NativeDebuggerSessionObserver
: public NativeDebuggerSessionObserverCxxSpec<
NativeDebuggerSessionObserver> {
public:
NativeDebuggerSessionObserver(std::shared_ptr<CallInvoker> jsInvoker);

bool hasActiveSession(jsi::Runtime& runtime);
std::function<void()> subscribe(
jsi::Runtime& runtime,
AsyncCallback<bool> callback);
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
* @oncall react_native
*/

import type {TurboModule} from '../../../../Libraries/TurboModule/RCTExport';

import * as TurboModuleRegistry from '../../../../Libraries/TurboModule/TurboModuleRegistry';

export interface Spec extends TurboModule {
+hasActiveSession: () => boolean;
+subscribe: (callback: (hasActiveSession: boolean) => void) => () => void;
}

export default (TurboModuleRegistry.get<Spec>(
'NativeDebuggerSessionObserverCxx',
): ?Spec);

0 comments on commit 6fde836

Please sign in to comment.