diff --git a/src/inspector/js_native_api_v8_inspector.cpp b/src/inspector/js_native_api_v8_inspector.cpp new file mode 100644 index 0000000000000000000000000000000000000000..215a2aecc0d7e8f1088b05b656f42bac0c921d8b --- /dev/null +++ b/src/inspector/js_native_api_v8_inspector.cpp @@ -0,0 +1,1332 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * 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. + */ + +#include "js_native_api_v8_inspector.h" + +#include + +#include "inspector_socket_server.h" +#include "inspector_utils.h" +#include "js_native_api_v8.h" +#include "jsvm_mutex.h" +#include "libplatform/libplatform.h" +#include "v8-inspector.h" +#include "v8-platform.h" + +#ifdef __POSIX__ +#include // PTHREAD_STACK_MIN +#include +#endif // __POSIX__ + +#include +#include +#include +#include +#include +#include + +namespace v8impl { + +namespace { +using jsvm::ConditionVariable; +using jsvm::Mutex; +using jsvm::inspector::StringViewToUtf8; +using jsvm::inspector::Utf8ToStringView; +using v8_inspector::StringBuffer; +using v8_inspector::StringView; + +class MainThreadInterface; + +class Request { +public: + virtual void Call(MainThreadInterface*) = 0; + virtual ~Request() = default; +}; + +class Deletable { +public: + virtual ~Deletable() = default; +}; + +using MessageQueue = std::deque>; + +class MainThreadHandle : public std::enable_shared_from_this { +public: + explicit MainThreadHandle(MainThreadInterface* mainThread) : mainThread(mainThread) {} + ~MainThreadHandle() + { + Mutex::ScopedLock scopedLock(blockLock); + CHECK_NULL(mainThread); // mainThread should have called Reset + } + std::unique_ptr Connect(std::unique_ptr delegate, bool preventShutdown); + int NewObjectId() + { + return ++nextObjectId; + } + bool Post(std::unique_ptr request); + +private: + void Reset(); + + MainThreadInterface* mainThread; + Mutex blockLock; + int nextSessionId = 0; + std::atomic_int nextObjectId = { 1 }; + + friend class MainThreadInterface; +}; + +class MainThreadInterface : public std::enable_shared_from_this { +public: + explicit MainThreadInterface(Agent* agent); + ~MainThreadInterface(); + + void DispatchMessages(); + void Post(std::unique_ptr request); + bool WaitForFrontendEvent(); + std::shared_ptr GetHandle(); + Agent* InspectorAgent() + { + return agent; + } + void AddObject(int handle, std::unique_ptr object); + Deletable* GetObject(int id); + Deletable* GetObjectIfExists(int id); + void RemoveObject(int handle); + +private: + MessageQueue requests; + Mutex requestsLock; // requests live across threads + // This queue is to maintain the order of the messages for the cases + // when we reenter the DispatchMessages function. + MessageQueue dispatchingMessageQueue; + bool dispatchingMessages = false; + ConditionVariable incomingMessageCond; + // Used from any thread + Agent* const agent; + std::shared_ptr handle; + std::unordered_map> managedObjects; +}; + +template +class DeletableWrapper : public Deletable { +public: + explicit DeletableWrapper(std::unique_ptr object) : object(std::move(object)) {} + ~DeletableWrapper() override = default; + + static T* Get(MainThreadInterface* thread, int id) + { + return static_cast*>(thread->GetObject(id))->object.get(); + } + +private: + std::unique_ptr object; +}; + +template +std::unique_ptr WrapInDeletable(std::unique_ptr object) +{ + return std::make_unique>(std::move(object)); +} + +template +class CreateObjectRequest : public Request { +public: + CreateObjectRequest(int objectId, Factory factory) : objectId(objectId), factory(std::move(factory)) {} + + void Call(MainThreadInterface* thread) override + { + thread->AddObject(objectId, WrapInDeletable(factory(thread))); + } + +private: + int objectId; + Factory factory; +}; + +template +std::unique_ptr NewCreateRequest(int objectId, Factory factory) +{ + return std::make_unique>(objectId, std::move(factory)); +} + +class DeleteRequest : public Request { +public: + explicit DeleteRequest(int objectId) : objectId(objectId) {} + + void Call(MainThreadInterface* thread) override + { + thread->RemoveObject(objectId); + } + +private: + int objectId; +}; + +template +class CallRequest : public Request { +public: + CallRequest(int id, Fn fn) : id(id), fn(std::move(fn)) {} + + void Call(MainThreadInterface* thread) override + { + fn(DeletableWrapper::Get(thread, id)); + } + +private: + int id; + Fn fn; +}; + +template +class AnotherThreadObjectReference { +public: + AnotherThreadObjectReference(std::shared_ptr thread, int objectId) + : thread(thread), objectId(objectId) + {} + + template + AnotherThreadObjectReference(std::shared_ptr thread, Factory factory) + : AnotherThreadObjectReference(thread, thread->NewObjectId()) + { + thread->Post(NewCreateRequest(objectId, std::move(factory))); + } + + AnotherThreadObjectReference(AnotherThreadObjectReference&) = delete; + + AnotherThreadObjectReference& operator=(const AnotherThreadObjectReference&) = delete; + + ~AnotherThreadObjectReference() + { + // Disappearing thread may cause a memory leak + thread->Post(std::make_unique(objectId)); + } + + template + void Call(Fn fn) const + { + using Request = CallRequest; + thread->Post(std::make_unique(objectId, std::move(fn))); + } + + template + void Call(void (T::*fn)(Arg), Arg argument) const + { + Call(std::bind(Apply, std::placeholders::_1, fn, std::move(argument))); + } + +private: + // This has to use non-const reference to support std::bind with non-copyable + // types + template + static void Apply(T* target, void (T::*fn)(Argument), Argument& argument) /* NOLINT (runtime/references) */ + { + (target->*fn)(std::move(argument)); + } + + std::shared_ptr thread; + const int objectId; +}; + +class MainThreadSessionState { +public: + MainThreadSessionState(MainThreadInterface* thread, bool preventShutdown) + : thread(thread), preventShutdown(preventShutdown) + {} + + static std::unique_ptr Create(MainThreadInterface* thread, bool preventShutdown) + { + return std::make_unique(thread, preventShutdown); + } + + void Connect(std::unique_ptr delegate) + { + Agent* agent = thread->InspectorAgent(); + if (agent != nullptr) { + session = agent->Connect(std::move(delegate), preventShutdown); + } + } + + void Dispatch(std::unique_ptr message) + { + session->Dispatch(message->string()); + } + +private: + MainThreadInterface* thread; + bool preventShutdown; + std::unique_ptr session; +}; + +class CrossThreadInspectorSession : public InspectorSession { +public: + CrossThreadInspectorSession(int id, + std::shared_ptr thread, + std::unique_ptr delegate, + bool preventShutdown) + : state(thread, std::bind(MainThreadSessionState::Create, std::placeholders::_1, preventShutdown)) + { + state.Call(&MainThreadSessionState::Connect, std::move(delegate)); + } + + void Dispatch(const StringView& message) override + { + state.Call(&MainThreadSessionState::Dispatch, StringBuffer::create(message)); + } + +private: + AnotherThreadObjectReference state; +}; + +class ThreadSafeDelegate : public InspectorSessionDelegate { +public: + ThreadSafeDelegate(std::shared_ptr thread, int objectId) + : thread(thread), delegate(thread, objectId) + {} + + void SendMessageToFrontend(const v8_inspector::StringView& message) override + { + delegate.Call([m = StringBuffer::create(message)](InspectorSessionDelegate* delegate) { + delegate->SendMessageToFrontend(m->string()); + }); + } + +private: + std::shared_ptr thread; + AnotherThreadObjectReference delegate; +}; + +MainThreadInterface::MainThreadInterface(Agent* agent) : agent(agent) {} + +MainThreadInterface::~MainThreadInterface() +{ + if (handle) { + handle->Reset(); + } +} + +void MainThreadInterface::Post(std::unique_ptr request) +{ + CHECK_NOT_NULL(agent); + Mutex::ScopedLock scopedLock(requestsLock); + bool needsNotify = requests.empty(); + requests.push_back(std::move(request)); + if (needsNotify) { + std::weak_ptr weakSelf { shared_from_this() }; + agent->env()->RequestInterrupt([weakSelf](Environment*) { + if (auto iface = weakSelf.lock()) { + iface->DispatchMessages(); + } + }); + } + incomingMessageCond.Broadcast(scopedLock); +} + +bool MainThreadInterface::WaitForFrontendEvent() +{ + // We allow DispatchMessages reentry as we enter the pause. This is important + // to support debugging the code invoked by an inspector call, such + // as Runtime.evaluate + dispatchingMessages = false; + if (dispatchingMessageQueue.empty()) { + Mutex::ScopedLock scopedLock(requestsLock); + while (requests.empty()) + incomingMessageCond.Wait(scopedLock); + } + return true; +} + +void MainThreadInterface::DispatchMessages() +{ + if (dispatchingMessages) { + return; + } + dispatchingMessages = true; + bool hadMessages = false; + do { + if (dispatchingMessageQueue.empty()) { + Mutex::ScopedLock scopedLock(requestsLock); + requests.swap(dispatchingMessageQueue); + } + hadMessages = !dispatchingMessageQueue.empty(); + while (!dispatchingMessageQueue.empty()) { + MessageQueue::value_type task; + std::swap(dispatchingMessageQueue.front(), task); + dispatchingMessageQueue.pop_front(); + + v8::SealHandleScope sealHandleScope(agent->env()->isolate); + task->Call(this); + } + } while (hadMessages); + dispatchingMessages = false; +} + +std::shared_ptr MainThreadInterface::GetHandle() +{ + if (handle == nullptr) { + handle = std::make_shared(this); + } + return handle; +} + +void MainThreadInterface::AddObject(int id, std::unique_ptr object) +{ + CHECK_NOT_NULL(object); + managedObjects[id] = std::move(object); +} + +void MainThreadInterface::RemoveObject(int handle) +{ + CHECK_EQ(1, managedObjects.erase(handle)); +} + +Deletable* MainThreadInterface::GetObject(int id) +{ + Deletable* pointer = GetObjectIfExists(id); + // This would mean the object is requested after it was disposed, which is + // a coding error. + CHECK_NOT_NULL(pointer); + return pointer; +} + +Deletable* MainThreadInterface::GetObjectIfExists(int id) +{ + auto iterator = managedObjects.find(id); + if (iterator == managedObjects.end()) { + return nullptr; + } + return iterator->second.get(); +} + +std::unique_ptr MainThreadHandle::Connect(std::unique_ptr delegate, + bool preventShutdown) +{ + return std::unique_ptr( + new CrossThreadInspectorSession(++nextSessionId, shared_from_this(), std::move(delegate), preventShutdown)); +} + +bool MainThreadHandle::Post(std::unique_ptr request) +{ + Mutex::ScopedLock scopedLock(blockLock); + if (!mainThread) { + return false; + } + mainThread->Post(std::move(request)); + return true; +} + +void MainThreadHandle::Reset() +{ + Mutex::ScopedLock scopedLock(blockLock); + mainThread = nullptr; +} +} // namespace + +namespace { +using jsvm::InspectPublishUid; +using jsvm::inspector::CheckedUvLoopClose; +using jsvm::inspector::CSPRNG; +using jsvm::inspector::FormatWsAddress; +using jsvm::inspector::GetHumanReadableProcessName; +using jsvm::inspector::InspectorSocketServer; + +// K_KILL closes connections and stops the server, K_STOP only stops the server +enum class TransportAction { K_KILL, K_SEND_MESSAGE, K_STOP }; + +std::string ScriptPath(uv_loop_t* loop, const std::string& scriptName) +{ + std::string scriptPath; + + if (!scriptName.empty()) { + uv_fs_t req; + req.ptr = nullptr; + if (0 == uv_fs_realpath(loop, &req, scriptName.c_str(), nullptr)) { + CHECK_NOT_NULL(req.ptr); + scriptPath = std::string(static_cast(req.ptr)); + } + uv_fs_req_cleanup(&req); + } + + return scriptPath; +} + +// UUID RFC: https://www.ietf.org/rfc/rfc4122.txt +// Used ver 4 - with numbers +std::string GenerateID() +{ + uint16_t buffer[8]; + CHECK(CSPRNG(buffer, sizeof(buffer))); + + char uuid[256]; + int ret = snprintf_s(uuid, sizeof(uuid), sizeof(uuid) - 1, "%04x%04x-%04x-%04x-%04x-%04x%04x%04x", + buffer[0], // time_low + buffer[1], // time_mid + buffer[2], // time_low + (buffer[3] & 0x0fff) | 0x4000, // time_hi_and_version + (buffer[4] & 0x3fff) | 0x8000, // clk_seq_hi clk_seq_low + buffer[5], // node + buffer[6], buffer[7]); + CHECK(ret >= 0); + return uuid; +} + +class RequestToServer { +public: + RequestToServer(TransportAction action, int sessionId, std::unique_ptr message) + : action(action), sessionId(sessionId), message(std::move(message)) + {} + + void Dispatch(InspectorSocketServer* server) const + { + switch (action) { + case TransportAction::K_KILL: + server->TerminateConnections(); + [[fallthrough]]; + case TransportAction::K_STOP: + server->Stop(); + break; + case TransportAction::K_SEND_MESSAGE: + server->Send(sessionId, StringViewToUtf8(message->string())); + break; + } + } + +private: + TransportAction action; + int sessionId; + std::unique_ptr message; +}; + +class RequestQueue; + +class RequestQueueData { +public: + using MessageQueue = std::deque; + + explicit RequestQueueData(uv_loop_t* loop) : handle(std::make_shared(this)) + { + int err = uv_async_init(loop, &async, [](uv_async_t* async) { + RequestQueueData* wrapper = jsvm::inspector::ContainerOf(&RequestQueueData::async, async); + wrapper->DoDispatch(); + }); + CHECK_EQ(0, err); + } + + static void CloseAndFree(RequestQueueData* queue); + + void Post(int sessionId, TransportAction action, std::unique_ptr message) + { + Mutex::ScopedLock scopedLock(stateLock); + bool notify = messages.empty(); + messages.emplace_back(action, sessionId, std::move(message)); + if (notify) { + CHECK_EQ(0, uv_async_send(&async)); + incomingMessageCond.Broadcast(scopedLock); + } + } + + void Wait() + { + Mutex::ScopedLock scopedLock(stateLock); + if (messages.empty()) { + incomingMessageCond.Wait(scopedLock); + } + } + + void SetServer(InspectorSocketServer* serverParam) + { + server = serverParam; + } + + std::shared_ptr GetHandle() + { + return handle; + } + +private: + ~RequestQueueData() = default; + + MessageQueue GetMessages() + { + Mutex::ScopedLock scopedLock(stateLock); + MessageQueue messagesQ; + messages.swap(messagesQ); + return messagesQ; + } + + void DoDispatch() + { + if (server == nullptr) { + return; + } + for (const auto& request : GetMessages()) { + request.Dispatch(server); + } + } + + std::shared_ptr handle; + uv_async_t async; + InspectorSocketServer* server = nullptr; + MessageQueue messages; + Mutex stateLock; // Locked before mutating the queue. + ConditionVariable incomingMessageCond; +}; + +class RequestQueue { +public: + explicit RequestQueue(RequestQueueData* data) : data(data) {} + + void Reset() + { + Mutex::ScopedLock scopedLock(lock); + data = nullptr; + } + + void Post(int sessionId, TransportAction action, std::unique_ptr message) + { + Mutex::ScopedLock scopedLock(lock); + if (data != nullptr) { + data->Post(sessionId, action, std::move(message)); + } + } + + bool Expired() + { + Mutex::ScopedLock scopedLock(lock); + return data == nullptr; + } + +private: + RequestQueueData* data; + Mutex lock; +}; + +class IoSessionDelegate : public InspectorSessionDelegate { +public: + explicit IoSessionDelegate(std::shared_ptr queue, int id) : requestQueue(queue), id(id) {} + void SendMessageToFrontend(const v8_inspector::StringView& message) override + { + requestQueue->Post(id, TransportAction::K_SEND_MESSAGE, StringBuffer::create(message)); + } + +private: + std::shared_ptr requestQueue; + int id; +}; + +// Passed to InspectorSocketServer to handle WS inspector protocol events, +// mostly session start, message received, and session end. +class InspectorIoDelegate : public jsvm::inspector::SocketServerDelegate { +public: + InspectorIoDelegate(std::shared_ptr queue, + std::shared_ptr mainThread, + const std::string& targetId, + const std::string& scriptPath, + const std::string& scriptName); + ~InspectorIoDelegate() override = default; + + void StartSession(int sessionId, const std::string& inTargetId) override; + void MessageReceived(int sessionId, const std::string& message) override; + void EndSession(int sessionId) override; + + std::vector GetTargetIds() override; + std::string GetTargetTitle(const std::string& id) override; + std::string GetTargetUrl(const std::string& id) override; + void AssignServer(InspectorSocketServer* server) override + { + requestQueue->SetServer(server); + } + +private: + std::shared_ptr requestQueue; + std::shared_ptr mainThread; + std::unordered_map> sessions; + const std::string scriptName; + const std::string scriptPath; + const std::string targetId; +}; + +InspectorIoDelegate::InspectorIoDelegate(std::shared_ptr queue, + std::shared_ptr mainThread, + const std::string& targetId, + const std::string& scriptPath, + const std::string& scriptName) + : requestQueue(queue), mainThread(mainThread), scriptName(scriptName), scriptPath(scriptPath), targetId(targetId) +{} + +void InspectorIoDelegate::StartSession(int sessionId, const std::string& inTargetId) +{ + auto session = mainThread->Connect(std::make_unique(requestQueue->GetHandle(), sessionId), true); + if (session) { + sessions[sessionId] = std::move(session); + if (fprintf(stderr, "Debugger attached.\n") < 0) { + return; + } + } +} + +void InspectorIoDelegate::MessageReceived(int sessionId, const std::string& message) +{ + auto session = sessions.find(sessionId); + if (session != sessions.end()) { + session->second->Dispatch(Utf8ToStringView(message)->string()); + } +} + +void InspectorIoDelegate::EndSession(int sessionId) +{ + sessions.erase(sessionId); +} + +std::vector InspectorIoDelegate::GetTargetIds() +{ + return { targetId }; +} + +std::string InspectorIoDelegate::GetTargetTitle(const std::string& id) +{ + return scriptName.empty() ? GetHumanReadableProcessName() : scriptName; +} + +std::string InspectorIoDelegate::GetTargetUrl(const std::string& id) +{ + return "file://" + scriptPath; +} + +// static +void RequestQueueData::CloseAndFree(RequestQueueData* queue) +{ + queue->handle->Reset(); + queue->handle.reset(); + uv_close(reinterpret_cast(&queue->async), [](uv_handle_t* handle) { + uv_async_t* async = reinterpret_cast(handle); + RequestQueueData* wrapper = jsvm::inspector::ContainerOf(&RequestQueueData::async, async); + delete wrapper; + }); +} +} // namespace + +class InspectorIo { +public: + // Start the inspector agent thread, waiting for it to initialize. + // Returns empty pointer if thread was not started. + static std::unique_ptr Start(std::shared_ptr mainThread, + const std::string& path, + std::shared_ptr> hostPortParam, + const jsvm::InspectPublishUid& inspectPublishUid); + + // Will block till the transport thread shuts down + ~InspectorIo(); + + void StopAcceptingNewConnections(); + std::string GetWsUrl() const; + +private: + InspectorIo(std::shared_ptr handle, + const std::string& path, + std::shared_ptr> hostPortParam, + const jsvm::InspectPublishUid& inspectPublishUid); + + // Wrapper for agent->ThreadMain() + static void ThreadMain(void* io); + + // Runs a uv_loop_t + void ThreadMain(); + + // This is a thread-safe object that will post async tasks. It lives as long + // as an Inspector object lives (almost as long as an Isolate). + std::shared_ptr mainThread; + // Used to post on a frontend interface thread, lives while the server is + // running + std::shared_ptr requestQueue; + std::shared_ptr> hostPort; + jsvm::InspectPublishUid inspectPublishUid; + + // The IO thread runs its own uv_loop to implement the TCP server off + // the main thread. + uv_thread_t thread; + + // For setting up interthread communications + Mutex threadStartLock; + jsvm::ConditionVariable threadStartCondition; + std::string scriptName; + // May be accessed from any thread + const std::string id; +}; + +// static +std::unique_ptr InspectorIo::Start(std::shared_ptr mainThread, + const std::string& path, + std::shared_ptr> hostPortParam, + const InspectPublishUid& inspectPublishUid) +{ + auto io = std::unique_ptr(new InspectorIo(mainThread, path, hostPortParam, inspectPublishUid)); + if (io->requestQueue->Expired()) { // Thread is not running + return nullptr; + } + return io; +} + +InspectorIo::InspectorIo(std::shared_ptr mainThread, + const std::string& path, + std::shared_ptr> hostPortParam, + const InspectPublishUid& inspectPublishUid) + : mainThread(mainThread), hostPort(hostPortParam), inspectPublishUid(inspectPublishUid), thread(), scriptName(path), + id(GenerateID()) +{ + Mutex::ScopedLock scopedLock(threadStartLock); + CHECK_EQ(uv_thread_create(&thread, InspectorIo::ThreadMain, this), 0); + threadStartCondition.Wait(scopedLock); +} + +InspectorIo::~InspectorIo() +{ + requestQueue->Post(0, TransportAction::K_KILL, nullptr); + int err = uv_thread_join(&thread); + CHECK_EQ(err, 0); +} + +void InspectorIo::StopAcceptingNewConnections() +{ + requestQueue->Post(0, TransportAction::K_STOP, nullptr); +} + +// static +void InspectorIo::ThreadMain(void* io) +{ + static_cast(io)->ThreadMain(); +} + +void InspectorIo::ThreadMain() +{ + uv_loop_t loop; + loop.data = nullptr; + int err = uv_loop_init(&loop); + CHECK_EQ(err, 0); + std::shared_ptr queue(new RequestQueueData(&loop), RequestQueueData::CloseAndFree); + std::string scriptPath = ScriptPath(&loop, scriptName); + std::unique_ptr delegate( + new InspectorIoDelegate(queue, mainThread, id, scriptPath, scriptName)); + std::string host; + int port; + int pid; + { + ExclusiveAccess::Scoped scopedHostPort(hostPort); + host = scopedHostPort->GetHost(); + port = scopedHostPort->GetPort(); + pid = scopedHostPort->GetPid(); + } + InspectorSocketServer server(std::move(delegate), &loop, std::move(host), port, inspectPublishUid, stderr, pid); + requestQueue = queue->GetHandle(); + // Its lifetime is now that of the server delegate + queue.reset(); + { + Mutex::ScopedLock scopedLock(threadStartLock); + if (server.Start()) { + ExclusiveAccess::Scoped scopedHostPort(hostPort); + scopedHostPort->SetPort(server.GetPort()); + } + threadStartCondition.Broadcast(scopedLock); + } + uv_run(&loop, UV_RUN_DEFAULT); + CheckedUvLoopClose(&loop); +} + +std::string InspectorIo::GetWsUrl() const +{ + ExclusiveAccess::Scoped scopedHostPort(hostPort); + return FormatWsAddress(scopedHostPort->GetHost(), scopedHostPort->GetPort(), id, true); +} + +namespace { + +using jsvm::inspector::TwoByteValue; + +using v8::Context; +using v8::Function; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Message; +using v8::Object; +using v8::Value; + +using v8_inspector::StringBuffer; +using v8_inspector::StringView; +using v8_inspector::V8Inspector; +using v8_inspector::V8InspectorClient; + +std::unique_ptr ToProtocolString(Isolate* isolate, Local value) +{ + TwoByteValue buffer(isolate, value); + return StringBuffer::create(StringView(*buffer, buffer.GetLength())); +} + +const int CONTEXT_GROUP_ID = 1; + +std::string GetWorkerLabel(Environment* env) +{ + std::ostringstream result; + result << "Worker[" + << "env->thread_id()" + << "]"; + return result.str(); +} + +class ChannelImpl final : public v8_inspector::V8Inspector::Channel { +public: + explicit ChannelImpl(const std::unique_ptr& inspector, + std::unique_ptr delegate, + std::shared_ptr mainThread, + bool preventShutdown) + : delegate(std::move(delegate)), preventShutdown(preventShutdown) + { + session = + inspector->connect(CONTEXT_GROUP_ID, this, StringView(), V8Inspector::ClientTrustLevel::kFullyTrusted); + } + + ~ChannelImpl() = default; + + void DispatchProtocolMessage(const StringView& message) + { + session->dispatchProtocolMessage(message); + } + + void SchedulePauseOnNextStatement(const std::string& reason) + { + std::unique_ptr buffer = Utf8ToStringView(reason); + session->schedulePauseOnNextStatement(buffer->string(), buffer->string()); + } + + bool PreventShutdown() + { + return preventShutdown; + } + +private: + void sendResponse(int callId, std::unique_ptr message) override + { + SendMessageToFrontend(message->string()); + } + + void sendNotification(std::unique_ptr message) override + { + SendMessageToFrontend(message->string()); + } + + void flushProtocolNotifications() override {} + + void SendMessageToFrontend(const StringView& message) + { + delegate->SendMessageToFrontend(message); + } + + void sendMessageToFrontend(const std::string& message) + { + SendMessageToFrontend(Utf8ToStringView(message)->string()); + } + + std::unique_ptr delegate; + std::unique_ptr session; + bool preventShutdown; +}; + +class SameThreadInspectorSession : public InspectorSession { +public: + SameThreadInspectorSession(int sessionId, std::shared_ptr client) + : sessionId(sessionId), client(client) + {} + ~SameThreadInspectorSession() override; + void Dispatch(const v8_inspector::StringView& message) override; + +private: + int sessionId; + std::weak_ptr client; +}; + +} // namespace + +class InspectorClient : public V8InspectorClient { +public: + explicit InspectorClient(Environment* env, bool isMain) : env(env), isMain(isMain) + { + client = V8Inspector::create(env->isolate, this); + std::string name = isMain ? GetHumanReadableProcessName() : GetWorkerLabel(env); + ContextInfo info(name); + info.isDefault = true; + ContextCreated(env->context(), info); + } + + void runMessageLoopOnPause(int contextGroupId) override + { + waitingForResume = true; + RunMessageLoop(); + } + + void WaitForSessionsDisconnect() + { + waitingForSessionsDisconnect = true; + RunMessageLoop(); + } + + void WaitForFrontend() + { + waitingForFrontend = true; + RunMessageLoop(); + } + + void maxAsyncCallStackDepthChanged(int depth) override + { + if (waitingForSessionsDisconnect) { + // V8 isolate is mostly done and is only letting Inspector protocol + // clients gather data. + return; + } + } + + void ContextCreated(Local context, const ContextInfo& info) + { + auto nameBuffer = Utf8ToStringView(info.name); + auto originBuffer = Utf8ToStringView(info.origin); + std::unique_ptr auxDataBuffer; + + v8_inspector::V8ContextInfo v8info(context, CONTEXT_GROUP_ID, nameBuffer->string()); + v8info.origin = originBuffer->string(); + + if (info.isDefault) { + auxDataBuffer = Utf8ToStringView("{\"isDefault\":true}"); + } else { + auxDataBuffer = Utf8ToStringView("{\"isDefault\":false}"); + } + v8info.auxData = auxDataBuffer->string(); + + client->contextCreated(v8info); + } + + void ContextDestroyed(Local context) + { + client->contextDestroyed(context); + } + + void quitMessageLoopOnPause() override + { + waitingForResume = false; + } + + void runIfWaitingForDebugger(int contextGroupId) override + { + waitingForFrontend = false; + } + + int ConnectFrontend(std::unique_ptr delegate, bool preventShutdown) + { + int sessionId = nextSessionId++; + channels[sessionId] = + std::make_unique(client, std::move(delegate), GetThreadHandle(), preventShutdown); + return sessionId; + } + + void DisconnectFrontend(int sessionId) + { + auto it = channels.find(sessionId); + if (it == channels.end()) { + return; + } + channels.erase(it); + if (waitingForSessionsDisconnect && !isMain) { + waitingForSessionsDisconnect = false; + } + } + + void DispatchMessageFromFrontend(int sessionId, const StringView& message) + { + channels[sessionId]->DispatchProtocolMessage(message); + } + + Local ensureDefaultContextInGroup(int contextGroupId) override + { + return env->context(); + } + + void ReportUncaughtException(Local error, Local message) + { + Isolate* isolate = env->isolate; + Local context = env->context(); + + int scriptId = message->GetScriptOrigin().ScriptId(); + + Local stackTrace = message->GetStackTrace(); + + if (!stackTrace.IsEmpty() && stackTrace->GetFrameCount() > 0 && + scriptId == stackTrace->GetFrame(isolate, 0)->GetScriptId()) { + scriptId = 0; + } + + const uint8_t details[] = "Uncaught"; + + client->exceptionThrown(context, StringView(details, sizeof(details) - 1), error, + ToProtocolString(isolate, message->Get())->string(), + ToProtocolString(isolate, message->GetScriptResourceName())->string(), + message->GetLineNumber(context).FromMaybe(0), + message->GetStartColumn(context).FromMaybe(0), client->createStackTrace(stackTrace), + scriptId); + } + + void startRepeatingTimer(double interval, TimerCallback callback, void* data) override {} + + void cancelTimer(void* data) override {} + + void SchedulePauseOnNextStatement(const std::string& reason) + { + for (const auto& idChannel : channels) { + idChannel.second->SchedulePauseOnNextStatement(reason); + } + } + + bool HasConnectedSessions() + { + for (const auto& idChannel : channels) { + // Other sessions are "invisible" more most purposes + if (idChannel.second->PreventShutdown()) { + return true; + } + } + return false; + } + + std::shared_ptr GetThreadHandle() + { + if (!interface) { + interface = std::make_shared(static_cast(env->GetInspectorAgent())); + } + return interface->GetHandle(); + } + + bool IsActive() + { + return !channels.empty(); + } + +private: + bool ShouldRunMessageLoop() + { + if (waitingForFrontend) { + return true; + } + if (waitingForSessionsDisconnect || waitingForResume) { + return HasConnectedSessions(); + } + return false; + } + + void RunMessageLoop() + { + if (runningNestedLoop) { + return; + } + + runningNestedLoop = true; + + while (ShouldRunMessageLoop()) { + if (interface) { + interface->WaitForFrontendEvent(); + } + env->RunAndClearInterrupts(); + } + runningNestedLoop = false; + } + + double currentTimeMS() override + { + return env->platform()->CurrentClockTimeMillis(); + } + + Environment* env; + bool isMain; + bool runningNestedLoop = false; + std::unique_ptr client; + std::unordered_map> channels; + int nextSessionId = 1; + bool waitingForResume = false; + bool waitingForFrontend = false; + bool waitingForSessionsDisconnect = false; + // Allows accessing Inspector from non-main threads + std::shared_ptr interface; +}; + +Agent::Agent(Environment* env) : parentEnv(env) {} + +Agent::~Agent() = default; + +bool Agent::Start(const std::string& pathParam, + std::shared_ptr> hostPortParam, + bool isMain, + bool waitForConnect) +{ + path = pathParam; + CHECK_NOT_NULL(hostPortParam); + hostPort = hostPortParam; + + client = std::make_shared(parentEnv, isMain); + + if (!StartIoThread()) { + return false; + } + + if (waitForConnect) { + client->WaitForFrontend(); + } + return true; +} + +int FindAvailablePort() +{ + constexpr int startPort = 9229; + constexpr int endPort = 9999; + constexpr int invalidPort = -1; + int sockfd = -1; + + for (auto port = startPort; port <= endPort; ++port) { + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) { + continue; + } + struct sockaddr_in addr; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(port); + + if (bind(sockfd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + close(sockfd); + if (errno == EADDRINUSE) { + continue; + } else { + break; + } + } + close(sockfd); + return port; + } + return invalidPort; +} + +bool Agent::Start(const std::string& pathParam, int pid) +{ + int port = FindAvailablePort(); + if (port < 0) { + return false; + } + auto hostPort = std::make_shared>("localhost", port, pid); + return Start(pathParam, hostPort, true, false); +} + +bool Agent::StartIoThread() +{ + if (io != nullptr) { + return true; + } + + if (!client) { + return false; + } + + CHECK_NOT_NULL(client); + + io = InspectorIo::Start(client->GetThreadHandle(), path, hostPort, { false, true }); + if (io == nullptr) { + return false; + } + return true; +} + +void Agent::Stop() +{ + io.reset(); +} + +std::unique_ptr Agent::Connect(std::unique_ptr delegate, + bool preventShutdown) +{ + if (!client) { + return std::unique_ptr {}; + } + + CHECK_NOT_NULL(client); + + int sessionId = client->ConnectFrontend(std::move(delegate), preventShutdown); + return std::make_unique(sessionId, client); +} + +void Agent::WaitForDisconnect() +{ + CHECK_NOT_NULL(client); + if (client->HasConnectedSessions()) { + if (fprintf(stderr, "Waiting for the debugger to disconnect...\n") < 0) { + return; + } + if (fflush(stderr) != 0) { + return; + } + } + + client->ContextDestroyed(parentEnv->context()); + + if (io != nullptr) { + io->StopAcceptingNewConnections(); + client->WaitForSessionsDisconnect(); + } +} + +void Agent::PauseOnNextJavascriptStatement(const std::string& reason) +{ + client->SchedulePauseOnNextStatement(reason); +} + +bool Agent::IsActive() +{ + if (client == nullptr) { + return false; + } + return io != nullptr || client->IsActive(); +} + +void Agent::WaitForConnect() +{ + CHECK_NOT_NULL(client); + client->WaitForFrontend(); +} + +SameThreadInspectorSession::~SameThreadInspectorSession() +{ + auto clientLock = client.lock(); + if (clientLock) { + clientLock->DisconnectFrontend(sessionId); + } +} + +void SameThreadInspectorSession::Dispatch(const v8_inspector::StringView& message) +{ + auto clientLock = client.lock(); + if (clientLock) { + clientLock->DispatchMessageFromFrontend(sessionId, message); + } +} + +} // namespace v8impl + +jsvm::InspectorAgent* jsvm::InspectorAgent::New(JSVM_Env env) +{ + return new v8impl::Agent(env); +} diff --git a/src/inspector/js_native_api_v8_inspector.h b/src/inspector/js_native_api_v8_inspector.h new file mode 100644 index 0000000000000000000000000000000000000000..238b4bb8a443dfb211ccc254396df842751212fc --- /dev/null +++ b/src/inspector/js_native_api_v8_inspector.h @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. + * 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. + */ + +#ifndef JS_NATIVE_API_V8_INSPECTOR_H +#define JS_NATIVE_API_V8_INSPECTOR_H + +#include +#include + +#include "js_native_api_v8.h" +#include "jsvm_host_port.h" +#include "jsvm_inspector_agent.h" +#include "v8.h" + +#ifndef ENABLE_INSPECTOR +#error("This header can only be used when inspector is enabled") +#endif + +namespace v8_inspector { +class StringView; +} // namespace v8_inspector + +namespace jsvm { +namespace inspector { +class InspectorSessionDelegate; +class InspectorSession; + +class InspectorSession { +public: + virtual ~InspectorSession() = default; + virtual void Dispatch(const v8_inspector::StringView& message) = 0; +}; + +class InspectorSessionDelegate { +public: + virtual ~InspectorSessionDelegate() = default; + virtual void SendMessageToFrontend(const v8_inspector::StringView& message) = 0; +}; +} // namespace inspector +} // namespace jsvm + +namespace v8impl { + +using jsvm::ExclusiveAccess; +using jsvm::HostPort; +using jsvm::inspector::InspectorSession; +using jsvm::inspector::InspectorSessionDelegate; + +class IsolateData; +using Environment = JSVM_Env__; + +class InspectorClient; +class InspectorIo; + +struct ContextInfo { + explicit ContextInfo(const std::string& name) : name(name) {} + const std::string name; + std::string origin; + bool isDefault = false; +}; + +class Agent : public jsvm::InspectorAgent { +public: + explicit Agent(Environment* env); + ~Agent(); + +public: + bool Start(const std::string& pathParam, const std::string& hostName, int port, int pid = -1) override + { + auto hostPort = std::make_shared>(hostName, port, pid); + return Start(pathParam, hostPort, true, false); + } + + bool Start(const std::string& pathParam, int pid) override; + + // Stop and destroy io_ + void Stop() override; + + // Returns true if the inspector is actually in use. + bool IsActive() override; + + // Blocks till frontend connects and sends "runIfWaitingForDebugger" + void WaitForConnect() override; + + // Blocks till all the sessions with "WaitForDisconnectOnShutdown" disconnect + void WaitForDisconnect() override; + + void PauseOnNextJavascriptStatement(const std::string& reason) override; + +public: + // Called to create inspector sessions that can be used from the same thread. + // The inspector responds by using the delegate to send messages back. + std::unique_ptr Connect(std::unique_ptr delegate, bool preventShutdown); + + // Can only be called from the main thread. + bool StartIoThread(); + + std::shared_ptr> GetHostPort() + { + return hostPort; + } + + inline Environment* env() const + { + return parentEnv; + } + +private: + // Create client_, may create io if option enabled + bool Start(const std::string& pathParam, + std::shared_ptr> hostPortParam, + bool isMain, + bool waitForConnect); + + Environment* parentEnv; + // Encapsulates majority of the Inspector functionality + std::shared_ptr client; + // Interface for transports, e.g. WebSocket server + std::unique_ptr io; + std::string path; + + std::shared_ptr> hostPort; +}; + +} // namespace v8impl + +#endif \ No newline at end of file