feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,6 @@
---
Checks: '>
clang-diagnostic-*,
'
InheritParentConfig: true
...

View File

@@ -0,0 +1,54 @@
# 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.
require "json"
# package.json
package = JSON.parse(File.read(File.join(__dir__, "..", "..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
folly_config = get_folly_config()
folly_compiler_flags = folly_config[:compiler_flags]
folly_version = folly_config[:version]
boost_compiler_flags = '-Wno-documentation'
Pod::Spec.new do |s|
s.name = "React-hermes"
s.version = version
s.summary = "Hermes engine for React Native"
s.homepage = "https://reactnative.dev/"
s.license = package['license']
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "executor/*.{cpp,h}",
"inspector-modern/chrome/*.{cpp,h}",
s.public_header_files = "executor/HermesExecutorFactory.h"
s.compiler_flags = folly_compiler_flags + ' ' + boost_compiler_flags
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"${PODS_ROOT}/hermes-engine/destroot/include\" \"$(PODS_TARGET_SRCROOT)/..\" \"$(PODS_ROOT)/boost\" \"$(PODS_ROOT)/RCT-Folly\" \"$(PODS_ROOT)/DoubleConversion\" \"$(PODS_ROOT)/fmt/include\"",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20"
}
s.header_dir = "reacthermes"
s.dependency "React-cxxreact", version
s.dependency "React-jsiexecutor", version
add_dependency(s, "React-jsinspector", :framework_name => 'jsinspector_modern')
s.dependency "React-perflogger", version
s.dependency "RCT-Folly", folly_version
s.dependency "DoubleConversion"
s.dependency "fmt", "9.1.0"
s.dependency "glog"
s.dependency "hermes-engine"
s.dependency "React-jsi"
s.dependency "React-runtimeexecutor"
end

View File

@@ -0,0 +1,37 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
add_compile_options(-std=c++20)
file(GLOB_RECURSE hermes_executor_SRC CONFIGURE_DEPENDS *.cpp)
add_library(
hermes_executor_common
STATIC
${hermes_executor_SRC}
)
target_include_directories(hermes_executor_common PUBLIC .)
target_link_libraries(hermes_executor_common
jsireact
hermes-engine::libhermes
jsi
hermes_inspector_modern
)
if(${CMAKE_BUILD_TYPE} MATCHES Debug)
target_compile_options(
hermes_executor_common
PRIVATE
-DHERMES_ENABLE_DEBUGGER=1
)
else()
target_compile_options(
hermes_executor_common
PRIVATE
-DNDEBUG
)
endif()

View File

@@ -0,0 +1,292 @@
/*
* 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 "HermesExecutorFactory.h"
#include <thread>
#include <cxxreact/MessageQueueThread.h>
#include <cxxreact/SystraceSection.h>
#include <hermes/hermes.h>
#include <jsi/decorator.h>
#include <jsinspector-modern/InspectorFlags.h>
#include <hermes/inspector-modern/chrome/HermesRuntimeAgentDelegate.h>
#include <hermes/inspector-modern/chrome/Registration.h>
#include <hermes/inspector/RuntimeAdapter.h>
using namespace facebook::hermes;
using namespace facebook::jsi;
namespace facebook::react {
namespace {
#ifdef HERMES_ENABLE_DEBUGGER
class HermesExecutorRuntimeAdapter
: public facebook::hermes::inspector_modern::RuntimeAdapter {
public:
HermesExecutorRuntimeAdapter(
std::shared_ptr<HermesRuntime> runtime,
std::shared_ptr<MessageQueueThread> thread)
: runtime_(runtime), thread_(std::move(thread)) {}
virtual ~HermesExecutorRuntimeAdapter() = default;
HermesRuntime& getRuntime() override {
return *runtime_;
}
void tickleJs() override {
thread_->runOnQueue(
[weakRuntime = std::weak_ptr<HermesRuntime>(runtime_)]() {
auto runtime = weakRuntime.lock();
if (!runtime) {
return;
}
jsi::Function func =
runtime->global().getPropertyAsFunction(*runtime, "__tickleJs");
func.call(*runtime);
});
}
private:
std::shared_ptr<HermesRuntime> runtime_;
std::shared_ptr<MessageQueueThread> thread_;
};
#endif // HERMES_ENABLE_DEBUGGER
struct ReentrancyCheck {
// This is effectively a very subtle and complex assert, so only
// include it in builds which would include asserts.
#ifndef NDEBUG
ReentrancyCheck() : tid(std::thread::id()), depth(0) {}
void before() {
std::thread::id this_id = std::this_thread::get_id();
std::thread::id expected = std::thread::id();
// A note on memory ordering: the main purpose of these checks is
// to observe a before/before race, without an intervening after.
// This will be detected by the compare_exchange_strong atomicity
// properties, regardless of memory order.
//
// For everything else, it is easiest to think of 'depth' as a
// proxy for any access made inside the VM. If access to depth
// are reordered incorrectly, the same could be true of any other
// operation made by the VM. In fact, using acquire/release
// memory ordering could create barriers which mask a programmer
// error. So, we use relaxed memory order, to avoid masking
// actual ordering errors. Although, in practice, ordering errors
// of this sort would be surprising, because the decorator would
// need to call after() without before().
if (tid.compare_exchange_strong(
expected, this_id, std::memory_order_relaxed)) {
// Returns true if tid and expected were the same. If they
// were, then the stored tid referred to no thread, and we
// atomically saved this thread's tid. Now increment depth.
assert(depth == 0 && "No thread id, but depth != 0");
++depth;
} else if (expected == this_id) {
// If the stored tid referred to a thread, expected was set to
// that value. If that value is this thread's tid, that's ok,
// just increment depth again.
assert(depth != 0 && "Thread id was set, but depth == 0");
++depth;
} else {
// The stored tid was some other thread. This indicates a bad
// programmer error, where VM methods were called on two
// different threads unsafely. Fail fast (and hard) so the
// crash can be analyzed.
__builtin_trap();
}
}
void after() {
assert(
tid.load(std::memory_order_relaxed) == std::this_thread::get_id() &&
"No thread id in after()");
if (--depth == 0) {
// If we decremented depth to zero, store no-thread into tid.
std::thread::id expected = std::this_thread::get_id();
bool didWrite = tid.compare_exchange_strong(
expected, std::thread::id(), std::memory_order_relaxed);
assert(didWrite && "Decremented to zero, but no tid write");
}
}
std::atomic<std::thread::id> tid;
// This is not atomic, as it is only written or read from the owning
// thread.
unsigned int depth;
#endif
};
// This adds ReentrancyCheck and debugger enable/teardown to the given
// Runtime.
class DecoratedRuntime : public jsi::WithRuntimeDecorator<ReentrancyCheck> {
public:
// The first argument may be another decorater which itself
// decorates the real HermesRuntime, depending on the build config.
// The second argument is the real HermesRuntime as well to
// manage the debugger registration.
DecoratedRuntime(
std::unique_ptr<Runtime> runtime,
HermesRuntime& hermesRuntime,
std::shared_ptr<MessageQueueThread> jsQueue,
bool enableDebugger,
const std::string& debuggerName)
: jsi::WithRuntimeDecorator<ReentrancyCheck>(*runtime, reentrancyCheck_),
runtime_(std::move(runtime)) {
#ifdef HERMES_ENABLE_DEBUGGER
enableDebugger_ = enableDebugger;
if (enableDebugger_) {
std::shared_ptr<HermesRuntime> rt(runtime_, &hermesRuntime);
auto adapter =
std::make_unique<HermesExecutorRuntimeAdapter>(rt, jsQueue);
debugToken_ = facebook::hermes::inspector_modern::chrome::enableDebugging(
std::move(adapter), debuggerName);
}
#endif // HERMES_ENABLE_DEBUGGER
}
~DecoratedRuntime() {
#ifdef HERMES_ENABLE_DEBUGGER
if (enableDebugger_) {
facebook::hermes::inspector_modern::chrome::disableDebugging(debugToken_);
}
#endif // HERMES_ENABLE_DEBUGGER
}
private:
// runtime_ is a potentially decorated Runtime.
// hermesRuntime is a reference to a HermesRuntime managed by runtime_.
//
// HermesExecutorRuntimeAdapter requirements are kept, because the
// dtor will disable debugging on the HermesRuntime before the
// member managing it is destroyed.
std::shared_ptr<Runtime> runtime_;
ReentrancyCheck reentrancyCheck_;
#ifdef HERMES_ENABLE_DEBUGGER
bool enableDebugger_;
facebook::hermes::inspector_modern::chrome::DebugSessionToken debugToken_;
#endif // HERMES_ENABLE_DEBUGGER
};
} // namespace
void HermesExecutorFactory::setEnableDebugger(bool enableDebugger) {
enableDebugger_ = enableDebugger;
}
void HermesExecutorFactory::setDebuggerName(const std::string& debuggerName) {
debuggerName_ = debuggerName;
}
std::unique_ptr<JSExecutor> HermesExecutorFactory::createJSExecutor(
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue) {
std::unique_ptr<HermesRuntime> hermesRuntime;
{
SystraceSection s("makeHermesRuntime");
hermesRuntime = hermes::makeHermesRuntime(runtimeConfig_);
}
HermesRuntime& hermesRuntimeRef = *hermesRuntime;
auto& inspectorFlags = jsinspector_modern::InspectorFlags::getInstance();
bool enableDebugger =
!inspectorFlags.getEnableModernCDPRegistry() && enableDebugger_;
auto decoratedRuntime = std::make_shared<DecoratedRuntime>(
std::move(hermesRuntime),
hermesRuntimeRef,
jsQueue,
enableDebugger,
debuggerName_);
// So what do we have now?
// DecoratedRuntime -> HermesRuntime
//
// DecoratedRuntime is held by JSIExecutor. When it gets used, it
// will check that it's on the right thread, do any necessary trace
// logging, then call the real HermesRuntime. When it is destroyed,
// it will shut down the debugger before the HermesRuntime is. In
// the normal case where debugging is not compiled in,
// all that's left is the thread checking.
// Add js engine information to Error.prototype so in error reporting we
// can send this information.
auto errorPrototype =
decoratedRuntime->global()
.getPropertyAsObject(*decoratedRuntime, "Error")
.getPropertyAsObject(*decoratedRuntime, "prototype");
errorPrototype.setProperty(*decoratedRuntime, "jsEngine", "hermes");
return std::make_unique<HermesExecutor>(
decoratedRuntime,
delegate,
jsQueue,
timeoutInvoker_,
runtimeInstaller_,
hermesRuntimeRef);
}
::hermes::vm::RuntimeConfig HermesExecutorFactory::defaultRuntimeConfig() {
return ::hermes::vm::RuntimeConfig::Builder()
.withEnableSampleProfiling(true)
.build();
}
HermesExecutor::HermesExecutor(
std::shared_ptr<jsi::Runtime> runtime,
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue,
const JSIScopedTimeoutInvoker& timeoutInvoker,
RuntimeInstaller runtimeInstaller,
HermesRuntime& hermesRuntime)
: JSIExecutor(runtime, delegate, timeoutInvoker, runtimeInstaller),
jsQueue_(jsQueue),
runtime_(runtime),
hermesRuntime_(hermesRuntime) {}
std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>
HermesExecutor::createAgentDelegate(
jsinspector_modern::FrontendChannel frontendChannel,
jsinspector_modern::SessionState& sessionState,
std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const jsinspector_modern::ExecutionContextDescription&
executionContextDescription) {
std::shared_ptr<HermesRuntime> hermesRuntimeShared(runtime_, &hermesRuntime_);
return std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>(
new jsinspector_modern::HermesRuntimeAgentDelegate(
frontendChannel,
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
hermesRuntimeShared,
[jsQueueWeak = std::weak_ptr(jsQueue_),
runtimeWeak = std::weak_ptr(runtime_)](auto fn) {
auto jsQueue = jsQueueWeak.lock();
if (!jsQueue) {
return;
}
jsQueue->runOnQueue([runtimeWeak, fn]() {
auto runtime = runtimeWeak.lock();
if (!runtime) {
return;
}
fn(*runtime);
});
}));
}
} // namespace facebook::react

View File

@@ -0,0 +1,73 @@
/*
* 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 <hermes/hermes.h>
#include <jsireact/JSIExecutor.h>
#include <utility>
namespace facebook::react {
class HermesExecutorFactory : public JSExecutorFactory {
public:
explicit HermesExecutorFactory(
JSIExecutor::RuntimeInstaller runtimeInstaller,
const JSIScopedTimeoutInvoker& timeoutInvoker =
JSIExecutor::defaultTimeoutInvoker,
::hermes::vm::RuntimeConfig runtimeConfig = defaultRuntimeConfig())
: runtimeInstaller_(runtimeInstaller),
timeoutInvoker_(timeoutInvoker),
runtimeConfig_(std::move(runtimeConfig)) {
assert(timeoutInvoker_ && "Should not have empty timeoutInvoker");
}
void setEnableDebugger(bool enableDebugger);
void setDebuggerName(const std::string& debuggerName);
std::unique_ptr<JSExecutor> createJSExecutor(
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue) override;
private:
static ::hermes::vm::RuntimeConfig defaultRuntimeConfig();
JSIExecutor::RuntimeInstaller runtimeInstaller_;
JSIScopedTimeoutInvoker timeoutInvoker_;
::hermes::vm::RuntimeConfig runtimeConfig_;
bool enableDebugger_ = true;
std::string debuggerName_ = "Hermes React Native";
};
class HermesExecutor : public JSIExecutor {
public:
HermesExecutor(
std::shared_ptr<jsi::Runtime> runtime,
std::shared_ptr<ExecutorDelegate> delegate,
std::shared_ptr<MessageQueueThread> jsQueue,
const JSIScopedTimeoutInvoker& timeoutInvoker,
RuntimeInstaller runtimeInstaller,
hermes::HermesRuntime& hermesRuntime);
virtual std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate>
createAgentDelegate(
jsinspector_modern::FrontendChannel frontendChannel,
jsinspector_modern::SessionState& sessionState,
std::unique_ptr<jsinspector_modern::RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const jsinspector_modern::ExecutionContextDescription&
executionContextDescription) override;
private:
JSIScopedTimeoutInvoker timeoutInvoker_;
std::shared_ptr<MessageQueueThread> jsQueue_;
std::shared_ptr<jsi::Runtime> runtime_;
hermes::HermesRuntime& hermesRuntime_;
};
} // namespace facebook::react

View File

@@ -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.
require "json"
package = JSON.parse(File.read(File.join(__dir__, "../../..", "package.json")))
version = package['version']
source = { :git => 'https://github.com/facebook/react-native.git' }
if version == '1000.0.0'
# This is an unpublished version, use the latest commit hash of the react-native repo, which were presumably in.
source[:commit] = `git rev-parse HEAD`.strip if system("git rev-parse --git-dir > /dev/null 2>&1")
else
source[:tag] = "v#{version}"
end
Pod::Spec.new do |s|
s.name = "React-jsitracing"
s.version = version
s.summary = "Internal library for JSI debugging."
s.homepage = "https://reactnative.dev/"
s.license = package["license"]
s.author = "Meta Platforms, Inc. and its affiliates"
s.platforms = min_supported_versions
s.source = source
s.source_files = "JSITracing.{cpp,h}"
s.header_dir = "."
s.pod_target_xcconfig = { "HEADER_SEARCH_PATHS" => "\"${PODS_TARGET_SRCROOT}/../..\"",
"USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
"GCC_WARN_PEDANTIC" => "YES" }
if ENV['USE_FRAMEWORKS']
s.header_mappings_dir = './'
s.module_name = 'React_jsitracing'
end
s.dependency "React-jsi"
end

View File

@@ -0,0 +1,37 @@
# 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.
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)
file(GLOB hermesinspectormodern_SRC CONFIGURE_DEPENDS chrome/*.cpp)
add_library(hermes_inspector_modern
STATIC
${hermesinspectormodern_SRC})
target_compile_options(
hermes_inspector_modern
PRIVATE
-std=c++20
-fexceptions
)
if(${CMAKE_BUILD_TYPE} MATCHES Debug)
target_compile_options(
hermes_inspector_modern
PRIVATE
-DHERMES_ENABLE_DEBUGGER=1
)
endif()
target_include_directories(hermes_inspector_modern PUBLIC ${REACT_COMMON_DIR})
target_link_libraries(hermes_inspector_modern
jsinspector
fb
glog
hermes-engine::libhermes
jsi
runtimeexecutor)

View File

@@ -0,0 +1,147 @@
/*
* 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 "ConnectionDemux.h"
#ifdef HERMES_ENABLE_DEBUGGER
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/CDPHandler.h>
#include <jsinspector-modern/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector_modern {
namespace chrome {
using ::facebook::react::jsinspector_modern::IInspector;
using ::facebook::react::jsinspector_modern::ILocalConnection;
using ::facebook::react::jsinspector_modern::IRemoteConnection;
namespace {
class LocalConnection : public ILocalConnection {
public:
LocalConnection(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn,
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts);
~LocalConnection();
void sendMessage(std::string message) override;
void disconnect() override;
private:
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn_;
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts_;
};
LocalConnection::LocalConnection(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn,
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts)
: conn_(conn), inspectedContexts_(inspectedContexts) {
inspectedContexts_->insert(conn->getTitle());
}
LocalConnection::~LocalConnection() = default;
void LocalConnection::sendMessage(std::string str) {
conn_->handle(std::move(str));
}
void LocalConnection::disconnect() {
inspectedContexts_->erase(conn_->getTitle());
conn_->unregisterCallbacks();
}
} // namespace
ConnectionDemux::ConnectionDemux(
facebook::react::jsinspector_modern::IInspector& inspector)
: globalInspector_(inspector),
inspectedContexts_(std::make_shared<std::unordered_set<std::string>>()) {}
ConnectionDemux::~ConnectionDemux() = default;
DebugSessionToken ConnectionDemux::enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string& title) {
std::scoped_lock lock(mutex_);
// TODO(#22976087): workaround for ComponentScript contexts never being
// destroyed.
//
// After a reload, the old ComponentScript VM instance stays alive. When we
// register the new CS VM instance, check for any previous CS VM (via strcmp
// of title) and remove them.
std::vector<int> pagesToDelete;
for (auto it = conns_.begin(); it != conns_.end(); ++it) {
if (it->second->getTitle() == title) {
pagesToDelete.push_back(it->first);
}
}
for (auto pageId : pagesToDelete) {
removePage(pageId);
}
auto waitForDebugger =
(inspectedContexts_->find(title) != inspectedContexts_->end());
return addPage(hermes::inspector_modern::chrome::CDPHandler::create(
std::move(adapter), title, waitForDebugger));
}
void ConnectionDemux::disableDebugging(DebugSessionToken session) {
std::scoped_lock lock(mutex_);
if (conns_.find(session) == conns_.end()) {
return;
}
removePage(session);
}
int ConnectionDemux::addPage(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn) {
auto connectFunc = [conn, this](std::unique_ptr<IRemoteConnection> remoteConn)
-> std::unique_ptr<ILocalConnection> {
// This cannot be unique_ptr as std::function is copyable but unique_ptr
// isn't. TODO: Change the CDPHandler API to accommodate this and not
// require a copyable callback?
std::shared_ptr<IRemoteConnection> sharedConn = std::move(remoteConn);
if (!conn->registerCallbacks(
[sharedConn](const std::string& message) {
sharedConn->onMessage(message);
},
[sharedConn]() { sharedConn->onDisconnect(); })) {
return nullptr;
}
return std::make_unique<LocalConnection>(conn, inspectedContexts_);
};
int pageId = globalInspector_.addPage(
conn->getTitle(), "Hermes", std::move(connectFunc));
conns_[pageId] = std::move(conn);
return pageId;
}
void ConnectionDemux::removePage(int pageId) {
globalInspector_.removePage(pageId);
auto conn = conns_.at(pageId);
std::string title = conn->getTitle();
inspectedContexts_->erase(title);
conn->unregisterCallbacks();
conns_.erase(pageId);
}
} // namespace chrome
} // namespace inspector_modern
} // namespace hermes
} // namespace facebook
#endif // HERMES_ENABLE_DEBUGGER

View File

@@ -0,0 +1,68 @@
/*
* 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
#ifdef HERMES_ENABLE_DEBUGGER
#include <memory>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <hermes/hermes.h>
#include <hermes/inspector-modern/chrome/Registration.h>
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/CDPHandler.h>
#include <jsinspector-modern/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector_modern {
namespace chrome {
/*
* ConnectionDemux keeps track of all debuggable Hermes runtimes (called
* "pages" in the higher-level React Native API) in this process. See
* Registration.h for documentation of the public API.
*/
class ConnectionDemux {
public:
explicit ConnectionDemux(
facebook::react::jsinspector_modern::IInspector& inspector);
~ConnectionDemux();
ConnectionDemux(const ConnectionDemux&) = delete;
ConnectionDemux& operator=(const ConnectionDemux&) = delete;
DebugSessionToken enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string& title);
void disableDebugging(DebugSessionToken session);
private:
int addPage(
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler> conn);
void removePage(int pageId);
facebook::react::jsinspector_modern::IInspector& globalInspector_;
std::mutex mutex_;
std::unordered_map<
int,
std::shared_ptr<hermes::inspector_modern::chrome::CDPHandler>>
conns_;
std::shared_ptr<std::unordered_set<std::string>> inspectedContexts_;
};
} // namespace chrome
} // namespace inspector_modern
} // namespace hermes
} // namespace facebook
#endif // HERMES_ENABLE_DEBUGGER

View File

@@ -0,0 +1,228 @@
/*
* 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 "HermesRuntimeAgentDelegate.h"
// If HERMES_ENABLE_DEBUGGER isn't defined, we can't access any Hermes
// CDPHandler headers or types.
#ifdef HERMES_ENABLE_DEBUGGER
#include <hermes/inspector/RuntimeAdapter.h>
#include <hermes/inspector/chrome/CDPHandler.h>
#else // HERMES_ENABLE_DEBUGGER
#include <jsinspector-modern/FallbackRuntimeAgentDelegate.h>
#endif // HERMES_ENABLE_DEBUGGER
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>
using namespace facebook::hermes;
namespace facebook::react::jsinspector_modern {
#ifdef HERMES_ENABLE_DEBUGGER
namespace {
/**
* An implementation of the Hermes RuntimeAdapter interface (part of
* Hermes's CDPHandler API) for use within a React Native RuntimeAgentDelegate.
*/
class HermesRuntimeAgentDelegateAdapter
: public hermes::inspector_modern::RuntimeAdapter {
public:
HermesRuntimeAgentDelegateAdapter(
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor)
: runtime_(runtime), runtimeExecutor_(runtimeExecutor) {}
HermesRuntime& getRuntime() override {
return *runtime_;
}
void tickleJs() override {
runtimeExecutor_([](jsi::Runtime& runtime) {
jsi::Function func =
runtime.global().getPropertyAsFunction(runtime, "__tickleJs");
func.call(runtime);
});
}
private:
std::shared_ptr<hermes::HermesRuntime> runtime_;
RuntimeExecutor runtimeExecutor_;
};
} // namespace
/**
* A RuntimeAgentDelegate that handles requests from the Chrome DevTools
* Protocol for an instance of Hermes.
*/
class HermesRuntimeAgentDelegate::Impl final : public RuntimeAgentDelegate {
using HermesCDPHandler = hermes::inspector_modern::chrome::CDPHandler;
using HermesExecutionContextDescription =
hermes::inspector_modern::chrome::CDPHandlerExecutionContextDescription;
using HermesState = hermes::inspector_modern::chrome::State;
struct HermesStateWrapper : public ExportedState {
explicit HermesStateWrapper(std::unique_ptr<HermesState> state)
: state_(std::move(state)) {}
static std::unique_ptr<HermesState> unwrapDestructively(
ExportedState* wrapper) {
if (!wrapper) {
return nullptr;
}
if (auto* typedWrapper = dynamic_cast<HermesStateWrapper*>(wrapper)) {
return std::move(typedWrapper->state_);
}
return nullptr;
}
private:
std::unique_ptr<HermesState> state_;
};
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param sessionState The state of the current CDP session. This will only
* be accessed on the main thread (during the constructor, in handleRequest,
* etc).
* \param previouslyExportedState The exported state from a previous instance
* of RuntimeAgentDelegate (NOT necessarily HermesRuntimeAgentDelegate). This
* may be nullptr, and if not nullptr it may be of any concrete type that
* implements RuntimeAgentDelegate::ExportedState.
* \param executionContextDescription A description of the execution context
* represented by this runtime. This is used for disambiguating the
* source/destination of CDP messages when there are multiple runtimes
* (concurrently or over the life of a Page).
* \param runtime The HermesRuntime that this agent is attached to.
* \param runtimeExecutor A callback for scheduling work on the JS thread.
* \c runtimeExecutor may drop scheduled work if the runtime is destroyed
* first.
*/
Impl(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor)
: hermes_(HermesCDPHandler::create(
std::make_unique<HermesRuntimeAgentDelegateAdapter>(
runtime,
runtimeExecutor),
/* waitForDebugger */ false,
/* enableConsoleAPICapturing */ false,
/* state */
HermesStateWrapper::unwrapDestructively(
previouslyExportedState.get()),
{.isRuntimeDomainEnabled = sessionState.isRuntimeDomainEnabled},
HermesExecutionContextDescription{
.id = executionContextDescription.id,
.origin = executionContextDescription.origin,
.name = executionContextDescription.name,
.auxData = std::nullopt,
.shouldSendNotifications = false})) {
hermes_->registerCallbacks(
/* msgCallback */
[frontendChannel =
std::move(frontendChannel)](const std::string& messageFromHermes) {
frontendChannel(messageFromHermes);
;
},
/* onUnregister */
[]() {});
}
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
* \param req The parsed request.
* \returns true if this agent has responded, or will respond asynchronously,
* to the request (with either a success or error message). False if the
* agent expects another agent to respond to the request instead.
*/
bool handleRequest(const cdp::PreparsedRequest& req) override {
// TODO: Change to string::starts_with when we're on C++20.
if (req.method.rfind("Log.", 0) == 0) {
// Since we know Hermes doesn't do anything useful with Log messages, but
// our containing PageAgent will, just bail out early.
// TODO: We need a way to negotiate this more dynamically with Hermes
// through the API.
return false;
}
// Forward everything else to Hermes's CDPHandler.
hermes_->handle(req.toJson());
// Let the call know that this request is handled (i.e. it is Hermes's
// responsibility to respond with either success or an error).
return true;
}
virtual std::unique_ptr<ExportedState> getExportedState() override {
return std::make_unique<HermesStateWrapper>(hermes_->getState());
}
private:
std::shared_ptr<HermesCDPHandler> hermes_;
};
#else // !HERMES_ENABLE_DEBUGGER
/**
* A stub for HermesRuntimeAgentDelegate when Hermes is compiled without
* debugging support.
*/
class HermesRuntimeAgentDelegate::Impl final
: public FallbackRuntimeAgentDelegate {
public:
Impl(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>,
const ExecutionContextDescription&,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor)
: FallbackRuntimeAgentDelegate(
std::move(frontendChannel),
sessionState,
runtime->description()) {}
};
#endif // HERMES_ENABLE_DEBUGGER
HermesRuntimeAgentDelegate::HermesRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor)
: impl_(std::make_unique<Impl>(
std::move(frontendChannel),
sessionState,
std::move(previouslyExportedState),
executionContextDescription,
std::move(runtime),
std::move(runtimeExecutor))) {}
bool HermesRuntimeAgentDelegate::handleRequest(
const cdp::PreparsedRequest& req) {
return impl_->handleRequest(req);
}
std::unique_ptr<HermesRuntimeAgentDelegate::ExportedState>
HermesRuntimeAgentDelegate::getExportedState() {
return impl_->getExportedState();
}
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,71 @@
/*
* 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 <ReactCommon/RuntimeExecutor.h>
#include <hermes/hermes.h>
#include <jsinspector-modern/ReactCdp.h>
namespace facebook::react::jsinspector_modern {
/**
* A RuntimeAgentDelegate that handles requests from the Chrome DevTools
* Protocol for an instance of Hermes.
*/
class HermesRuntimeAgentDelegate : public RuntimeAgentDelegate {
public:
/**
* \param frontendChannel A channel used to send responses and events to the
* frontend.
* \param sessionState The state of the current CDP session. This will only
* be accessed on the main thread (during the constructor, in handleRequest,
* etc).
* \param previouslyExportedState The exported state from a previous instance
* of RuntimeAgentDelegate (NOT necessarily HermesRuntimeAgentDelegate). This
* may be nullptr, and if not nullptr it may be of any concrete type that
* implements RuntimeAgentDelegate::ExportedState.
* \param executionContextDescription A description of the execution context
* represented by this runtime. This is used for disambiguating the
* source/destination of CDP messages when there are multiple runtimes
* (concurrently or over the life of a Page).
* \param runtime The HermesRuntime that this agent is attached to.
* \param runtimeExecutor A callback for scheduling work on the JS thread.
* \c runtimeExecutor may drop scheduled work if the runtime is destroyed
* first.
*/
HermesRuntimeAgentDelegate(
FrontendChannel frontendChannel,
SessionState& sessionState,
std::unique_ptr<RuntimeAgentDelegate::ExportedState>
previouslyExportedState,
const ExecutionContextDescription& executionContextDescription,
std::shared_ptr<hermes::HermesRuntime> runtime,
RuntimeExecutor runtimeExecutor);
/**
* Handle a CDP request. The response will be sent over the provided
* \c FrontendChannel synchronously or asynchronously.
* \param req The parsed request.
* \returns true if this agent has responded, or will respond asynchronously,
* to the request (with either a success or error message). False if the
* agent expects another agent to respond to the request instead.
*/
bool handleRequest(const cdp::PreparsedRequest& req) override;
virtual std::unique_ptr<ExportedState> getExportedState() override;
private:
// We use the private implementation idiom to keep HERMES_ENABLE_DEBUGGER
// checks out of the header.
class Impl;
const std::unique_ptr<Impl> impl_;
};
} // namespace facebook::react::jsinspector_modern

View File

@@ -0,0 +1,43 @@
/*
* 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 "Registration.h"
#include "ConnectionDemux.h"
#ifdef HERMES_ENABLE_DEBUGGER
namespace facebook {
namespace hermes {
namespace inspector_modern {
namespace chrome {
namespace {
ConnectionDemux& demux() {
static ConnectionDemux instance{
facebook::react::jsinspector_modern::getInspectorInstance()};
return instance;
}
} // namespace
DebugSessionToken enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string& title) {
return demux().enableDebugging(std::move(adapter), title);
}
void disableDebugging(DebugSessionToken session) {
demux().disableDebugging(session);
}
} // namespace chrome
} // namespace inspector_modern
} // namespace hermes
} // namespace facebook
#endif // HERMES_ENABLE_DEBUGGER

View File

@@ -0,0 +1,47 @@
/*
* 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
#ifdef HERMES_ENABLE_DEBUGGER
#include <memory>
#include <string>
#include <hermes/hermes.h>
#include <hermes/inspector/RuntimeAdapter.h>
namespace facebook {
namespace hermes {
namespace inspector_modern {
namespace chrome {
using DebugSessionToken = int;
/*
* enableDebugging adds this runtime to the list of debuggable JS targets
* (called "pages" in the higher-level React Native API) in this process. It
* should be called before any JS runs in the runtime. The returned token
* can be used to disable debugging for this runtime.
*/
extern DebugSessionToken enableDebugging(
std::unique_ptr<RuntimeAdapter> adapter,
const std::string& title);
/*
* disableDebugging removes this runtime from the list of debuggable JS targets
* in this process. The runtime to remove is identified by the token returned
* from enableDebugging.
*/
extern void disableDebugging(DebugSessionToken session);
} // namespace chrome
} // namespace inspector_modern
} // namespace hermes
} // namespace facebook
#endif // HERMES_ENABLE_DEBUGGER

View File

@@ -0,0 +1,148 @@
/*
* 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 <chrono>
#include <condition_variable>
#include <mutex>
#include <string>
#include <unordered_map>
#include <vector>
#include <gtest/gtest.h>
#include <hermes/hermes.h>
#include <hermes/inspector-modern/chrome/ConnectionDemux.h>
#include <jsinspector-modern/InspectorInterfaces.h>
namespace facebook {
namespace hermes {
namespace inspector_modern {
namespace chrome {
using ::facebook::react::jsinspector_modern::IInspector;
using ::facebook::react::jsinspector_modern::InspectorPageDescription;
using ::facebook::react::jsinspector_modern::IRemoteConnection;
namespace {
std::unordered_map<int, std::string> makePageMap(
const std::vector<InspectorPageDescription>& pages) {
std::unordered_map<int, std::string> pageMap;
for (auto& page : pages) {
pageMap[page.id] = page.title;
}
return pageMap;
}
void expectPages(
IInspector& inspector,
const std::unordered_map<int, std::string>& expected) {
auto pages = makePageMap(inspector.getPages());
EXPECT_EQ(pages, expected);
}
class TestRemoteConnection : public IRemoteConnection {
public:
class Data {
public:
void expectDisconnected() {
std::unique_lock<std::mutex> lock(mutex_);
cv_.wait_for(
lock, std::chrono::milliseconds(2500), [&] { return !connected_; });
EXPECT_FALSE(connected_);
}
void setDisconnected() {
std::scoped_lock lock(mutex_);
connected_ = false;
cv_.notify_one();
}
private:
std::mutex mutex_;
std::condition_variable cv_;
bool connected_{true};
};
TestRemoteConnection() : data_(std::make_shared<Data>()) {}
~TestRemoteConnection() {}
void onMessage(std::string message) override {}
void onDisconnect() override {
data_->setDisconnected();
}
std::shared_ptr<Data> getData() {
return data_;
}
private:
std::shared_ptr<Data> data_;
};
}; // namespace
TEST(ConnectionDemuxTests, TestEnableDisable) {
std::shared_ptr<HermesRuntime> runtime1(
facebook::hermes::makeHermesRuntime());
std::shared_ptr<HermesRuntime> runtime2(
facebook::hermes::makeHermesRuntime());
auto inspector =
facebook::react::jsinspector_modern::makeTestInspectorInstance();
ConnectionDemux demux{*inspector};
int id1 = demux.enableDebugging(
std::make_unique<SharedRuntimeAdapter>(runtime1), "page1");
int id2 = demux.enableDebugging(
std::make_unique<SharedRuntimeAdapter>(runtime2), "page2");
expectPages(*inspector, {{id1, "page1"}, {id2, "page2"}});
auto remoteConn1 = std::make_unique<TestRemoteConnection>();
auto remoteData1 = remoteConn1->getData();
auto localConn1 = inspector->connect(id1, std::move(remoteConn1));
EXPECT_NE(localConn1.get(), nullptr);
{
// If we connect to the same page id again without disconnecting, we should
// get null
auto remoteConn = std::make_unique<TestRemoteConnection>();
auto localConn = inspector->connect(id1, std::move(remoteConn));
EXPECT_EQ(localConn.get(), nullptr);
}
auto remoteConn2 = std::make_unique<TestRemoteConnection>();
auto remoteData2 = remoteConn2->getData();
auto localConn2 = inspector->connect(id2, std::move(remoteConn2));
EXPECT_NE(localConn2.get(), nullptr);
// Disable debugging on runtime2. This should remove its page from the list
// and call onDisconnect on its remoteConn
demux.disableDebugging(id2);
expectPages(*inspector, {{id1, "page1"}});
remoteData2->expectDisconnected();
// Disconnect conn1. Its page should still be in the page list and
// onDisconnect should be called.
localConn1->disconnect();
remoteData1->expectDisconnected();
{
// Should still be able to reconnect after disconnecting
auto remoteConn = std::make_unique<TestRemoteConnection>();
auto localConn = inspector->connect(id1, std::move(remoteConn));
EXPECT_NE(localConn.get(), nullptr);
}
}
} // namespace chrome
} // namespace inspector_modern
} // namespace hermes
} // namespace facebook