From 73da163ba8a6d97cbc068c045b48aa418f14f12f Mon Sep 17 00:00:00 2001 From: Eduardo Speroni Date: Fri, 23 Oct 2026 13:58:13 -0300 Subject: [PATCH] feat(inspector): serve multiple DevTools clients at once Each connection now gets its own V8InspectorSession on the main isolate and on every worker, and sees only replies to its own commands, as with Chrome and Node. InspectorSessions keeps the per-connection sessions for one isolate; connect, disconnect and messages share one queue, so a client's session exists before its first message and outlives its last. A departing client's session is destroyed, which disables its agents: a pause ends only if no other client still has the debugger enabled. A session dispatching when its client leaves (the pause loop can run under its own Runtime.evaluate) gets Debugger.disable at once and is destroyed when that dispatch returns. Per-client state: Target auto-attach and announced workers, IO streams, and tracing (one owner at a time). @nativescript/core's JS domains are global, so their events go to every client and a .disable only reaches the handler once no other client has the domain enabled. The server no longer closes the previous client. It drops a message read just before a write error closed its connection, closes a client more than 256 MB behind on writes, and no longer logs connection resets and broken pipes. Also fix a trap in libdispatch when a message dispatched by the drain pauses (e.g. evaluating `debugger` in the console): the drain held messageLoopQueue_ while dispatching and the pause loop dispatch_sync'd onto it. The drain now only reads the pause-loop flag under that queue. This replaces the single-client sync sentinel, session rebinding and frontend generations. --- NativeScript/inspector/InspectorServer.h | 10 +- NativeScript/inspector/InspectorServer.mm | 62 +- NativeScript/inspector/InspectorSessions.h | 82 +++ NativeScript/inspector/InspectorSessions.mm | 160 +++++ NativeScript/inspector/JsV8InspectorClient.h | 172 ++--- NativeScript/inspector/JsV8InspectorClient.mm | 612 +++++++----------- .../inspector/WorkerInspectorClient.h | 67 +- .../inspector/WorkerInspectorClient.mm | 131 ++-- v8ios.xcodeproj/project.pbxproj | 8 + 9 files changed, 700 insertions(+), 604 deletions(-) create mode 100644 NativeScript/inspector/InspectorSessions.h create mode 100644 NativeScript/inspector/InspectorSessions.mm diff --git a/NativeScript/inspector/InspectorServer.h b/NativeScript/inspector/InspectorServer.h index 41e65834..f3c2e4e1 100644 --- a/NativeScript/inspector/InspectorServer.h +++ b/NativeScript/inspector/InspectorServer.h @@ -18,12 +18,10 @@ class InspectorServer { // process-unique connection id: onClientConnected hands out its sender, // and onClientDisconnected fires once with the same id when the // connection closes, after which that sender drops every message. - // One client at a time: accepting a client closes the previous one, and - // that close reports onClientDisconnected before the new onClientConnected. - // A client that closed itself (EOF, read or write error) reports from its - // own queue, which can interleave with a new onClientConnected. A message - // the previous client had already read may reach onMessage after the - // takeover, so callers must check the connection id. + // Any number of clients may be connected at once. For one connection the + // callbacks are ordered: onClientConnected returns before its first + // onMessage, and onClientDisconnected comes after its last. Callbacks for + // different connections run concurrently. static in_port_t Init( std::function onClientConnected, diff --git a/NativeScript/inspector/InspectorServer.mm b/NativeScript/inspector/InspectorServer.mm index 5c539806..b748886c 100644 --- a/NativeScript/inspector/InspectorServer.mm +++ b/NativeScript/inspector/InspectorServer.mm @@ -12,15 +12,29 @@ std::atomic gLastConnectionId{0}; +// A client this far behind is assumed gone (e.g. a half-open socket whose +// peer vanished without a FIN): every client receives broadcast events, so +// its unsent output would otherwise grow without bound. Large enough for a +// heap snapshot streamed over a slow link. +constexpr size_t kMaxPendingWriteBytes = 256 * 1024 * 1024; + struct Connection { uint64_t id; dispatch_queue_t queue; dispatch_io_t channel; + // Only changes on `queue`. std::atomic open{true}; + std::atomic pendingWriteBytes{0}; std::function onDisconnected; std::function onMessage; }; +// The peer went away or we closed the channel ourselves (ECANCELED: an +// operation was still queued at close time); routine, not worth a log line. +bool IsDisconnectError(int error) { + return error == ECANCELED || error == ECONNRESET || error == EPIPE; +} + void CloseConnection(const std::shared_ptr& connection) { if (!connection->open.exchange(false)) { return; @@ -46,7 +60,7 @@ void ReadExactly(std::shared_ptr connection, size_t length, return; } if (error != 0 || dispatch_data_get_size(received) < length) { - if (error != 0 && error != ECANCELED) { + if (error != 0 && !IsDisconnectError(error)) { NSLog(@"InspectorServer read error: %s", strerror(error)); } CloseConnection(connection); @@ -70,6 +84,11 @@ void ReadNextMessage(std::shared_ptr connection) { ReadExactly(connection, length, ^(dispatch_data_t body) { NSString* payload = [[NSString alloc] initWithData:(NSData*)body encoding:NSUTF16LittleEndianStringEncoding]; + // A write error may have closed the connection after this read + // completed; onMessage must never follow onDisconnected. + if (!connection->open.load()) { + return; + } const char* utf8 = [payload UTF8String]; if (utf8 != nullptr) { connection->onMessage(connection->id, utf8); @@ -99,19 +118,31 @@ void Send(std::shared_ptr connection, const std::string& message) { range:NSMakeRange(0, str.length) remainingRange:NULL]; - dispatch_data_t data = - dispatch_data_create(buffer, length + sizeof(uint32_t), connection->queue, ^{ - free(buffer); - }); + size_t size = length + sizeof(uint32_t); + dispatch_data_t data = dispatch_data_create(buffer, size, connection->queue, ^{ + free(buffer); + }); + + if (connection->pendingWriteBytes.fetch_add(size) + size > kMaxPendingWriteBytes) { + NSLog(@"InspectorServer: closing a client that stopped reading"); + // Closed on its own queue to keep onDisconnected ordered after onMessage. + dispatch_async(connection->queue, ^{ + CloseConnection(connection); + }); + return; + } dispatch_io_write(connection->channel, 0, data, connection->queue, ^(bool done, dispatch_data_t data, int error) { - // ECANCELED: the write was still queued when the - // connection closed. - if (error == 0 || error == ECANCELED) { + if (done) { + connection->pendingWriteBytes.fetch_sub(size); + } + if (error == 0) { return; } - NSLog(@"InspectorServer::Send error: %s", strerror(error)); + if (!IsDisconnectError(error)) { + NSLog(@"InspectorServer::Send error: %s", strerror(error)); + } CloseConnection(connection); }); } @@ -145,10 +176,6 @@ void Send(std::shared_ptr connection, const std::string& message) { assert(false); } - // Only touched by the accept handler, which libdispatch never runs - // concurrently with itself. - __block std::weak_ptr activeConnection; - // Set up the dispatch source that will alert us to new incoming connections dispatch_queue_t q = dispatch_queue_create("server_queue", DISPATCH_QUEUE_CONCURRENT); dispatch_source_t acceptSource = @@ -190,13 +217,8 @@ void Send(std::shared_ptr connection, const std::string& message) { continue; } - // The accept handler is serial, so back-to-back accepts take over in - // order and the newest client stays active. - std::shared_ptr previous = activeConnection.lock(); - activeConnection = connection; - if (previous != nullptr) { - CloseConnection(previous); - } + // Reads start only after this returns, so the client's first message + // cannot overtake its own connect. onClientConnected(connection->id, [connection](const std::string& message) { Send(connection, message); }); diff --git a/NativeScript/inspector/InspectorSessions.h b/NativeScript/inspector/InspectorSessions.h new file mode 100644 index 00000000..b554d149 --- /dev/null +++ b/NativeScript/inspector/InspectorSessions.h @@ -0,0 +1,82 @@ +#ifndef InspectorSessions_h +#define InspectorSessions_h + +#include +#include +#include +#include +#include +#include +#include + +#include "include/v8-inspector.h" + +namespace v8_inspector { + +// One V8InspectorSession per connected frontend, all on one isolate and +// context group. Each session's output goes only to its own frontend. +// +// Except for RequestPause and ClearPauseRequests, call only from the one +// thread at a time that feeds the isolate's inspector messages (its message +// drain or pause loop), or from an interrupt on the thread running its JS. +// The methods take the isolate's Locker themselves (v8::Locker is reentrant). +class InspectorSessions { + public: + using Output = + std::function; + + InspectorSessions(v8::Isolate* isolate, int contextGroupId, Output output); + ~InspectorSessions(); + + // Creates the frontend's session unless it already has one. + void Connect(V8Inspector* inspector, uint64_t connectionId); + + // Ignored for a frontend without a session or one being closed. + void Dispatch(uint64_t connectionId, const std::string& message); + + // Destroys the frontend's session. Its destructor disables its agents, so + // a pause ends only if no other frontend still has the debugger enabled. + // A session cannot be destroyed while it is dispatching (a pause loop can + // sit under its dispatchProtocolMessage); it then gets Debugger.disable + // right away and is destroyed once that dispatch returns. + void Disconnect(uint64_t connectionId); + + // Destroys every session. Never while any of them is dispatching. + void DisconnectAll(); + + bool Has(uint64_t connectionId) const; + + // Any thread. Records that the frontend asked to pause; TakePauseRequests, + // called from an isolate interrupt, schedules the pause in its session. + void RequestPause(uint64_t connectionId); + void TakePauseRequests(); + // Any thread. Called when a pause starts, so a request whose interrupt is + // still pending cannot pause again after the resume. + void ClearPauseRequests(); + + void SchedulePauseInAll(); + + private: + class Channel; + struct Session { + std::unique_ptr channel; + std::unique_ptr session; + int dispatchDepth = 0; + bool closing = false; + }; + + void DispatchTo(Session* session, const std::string& message); + void Destroy(uint64_t connectionId); + + v8::Isolate* isolate_; + int contextGroupId_; + Output output_; + std::map> sessions_; + + std::mutex pauseRequestsMutex_; + std::vector pauseRequests_; // guarded by pauseRequestsMutex_ +}; + +} // namespace v8_inspector + +#endif /* InspectorSessions_h */ diff --git a/NativeScript/inspector/InspectorSessions.mm b/NativeScript/inspector/InspectorSessions.mm new file mode 100644 index 00000000..7cfde1bc --- /dev/null +++ b/NativeScript/inspector/InspectorSessions.mm @@ -0,0 +1,160 @@ +#include "InspectorSessions.h" + +#include "include/v8-locker.h" +#include "utils.h" + +using namespace v8; + +namespace v8_inspector { + +namespace { +StringView Make8BitStringView(const std::string& value) { + return StringView(reinterpret_cast(value.data()), value.size()); +} + +constexpr const char* kDisableDebugger = "{\"id\":0,\"method\":\"Debugger.disable\"}"; +} // namespace + +class InspectorSessions::Channel final : public V8Inspector::Channel { + public: + Channel(const Output& output, uint64_t connectionId) + : output_(output), connectionId_(connectionId) {} + + void sendResponse(int callId, std::unique_ptr message) override { + output_(connectionId_, ToStdString(message->string())); + } + + void sendNotification(std::unique_ptr message) override { + output_(connectionId_, ToStdString(message->string())); + } + + void flushProtocolNotifications() override {} + + private: + const Output& output_; + uint64_t connectionId_; +}; + +InspectorSessions::InspectorSessions(Isolate* isolate, int contextGroupId, Output output) + : isolate_(isolate), contextGroupId_(contextGroupId), output_(std::move(output)) {} + +InspectorSessions::~InspectorSessions() { this->DisconnectAll(); } + +void InspectorSessions::Connect(V8Inspector* inspector, uint64_t connectionId) { + if (sessions_.count(connectionId) != 0) { + return; + } + + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + + auto entry = std::make_unique(); + entry->channel = std::make_unique(output_, connectionId); + entry->session = + inspector->connect(contextGroupId_, entry->channel.get(), {}, V8Inspector::kFullyTrusted); + sessions_.emplace(connectionId, std::move(entry)); +} + +void InspectorSessions::Dispatch(uint64_t connectionId, const std::string& message) { + auto it = sessions_.find(connectionId); + if (it == sessions_.end() || it->second->closing) { + return; + } + + Session* session = it->second.get(); + this->DispatchTo(session, message); + if (session->closing && session->dispatchDepth == 0) { + this->Destroy(connectionId); + } +} + +void InspectorSessions::DispatchTo(Session* session, const std::string& message) { + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + + // Destroy never runs while this is above zero, so `session` stays valid + // across the call even if the frontend disconnects inside it. + session->dispatchDepth++; + session->session->dispatchProtocolMessage(Make8BitStringView(message)); + session->dispatchDepth--; +} + +void InspectorSessions::Disconnect(uint64_t connectionId) { + auto it = sessions_.find(connectionId); + if (it == sessions_.end()) { + return; + } + + Session* session = it->second.get(); + if (session->dispatchDepth == 0) { + this->Destroy(connectionId); + return; + } + + if (!session->closing) { + session->closing = true; + this->DispatchTo(session, kDisableDebugger); + } +} + +void InspectorSessions::DisconnectAll() { + while (!sessions_.empty()) { + this->Destroy(sessions_.begin()->first); + } +} + +bool InspectorSessions::Has(uint64_t connectionId) const { + auto it = sessions_.find(connectionId); + return it != sessions_.end() && !it->second->closing; +} + +void InspectorSessions::Destroy(uint64_t connectionId) { + auto node = sessions_.extract(connectionId); + if (node.empty()) { + return; + } + + Locker locker(isolate_); + Isolate::Scope isolate_scope(isolate_); + HandleScope handle_scope(isolate_); + // The session uses its channel until its destructor returns. + node.mapped()->session.reset(); + node.mapped()->channel.reset(); +} + +void InspectorSessions::RequestPause(uint64_t connectionId) { + std::lock_guard lock(pauseRequestsMutex_); + pauseRequests_.push_back(connectionId); +} + +void InspectorSessions::TakePauseRequests() { + std::vector requests; + { + std::lock_guard lock(pauseRequestsMutex_); + requests.swap(pauseRequests_); + } + + for (uint64_t connectionId : requests) { + auto it = sessions_.find(connectionId); + if (it != sessions_.end() && !it->second->closing) { + it->second->session->schedulePauseOnNextStatement({}, {}); + } + } +} + +void InspectorSessions::ClearPauseRequests() { + std::lock_guard lock(pauseRequestsMutex_); + pauseRequests_.clear(); +} + +void InspectorSessions::SchedulePauseInAll() { + for (auto& entry : sessions_) { + if (!entry.second->closing) { + entry.second->session->schedulePauseOnNextStatement({}, {}); + } + } +} + +} // namespace v8_inspector diff --git a/NativeScript/inspector/JsV8InspectorClient.h b/NativeScript/inspector/JsV8InspectorClient.h index 9be37467..25173020 100644 --- a/NativeScript/inspector/JsV8InspectorClient.h +++ b/NativeScript/inspector/JsV8InspectorClient.h @@ -4,13 +4,16 @@ #include #include -#include #include #include +#include #include +#include +#include #include #include +#include "InspectorSessions.h" #include "include/v8-inspector.h" #include "ns-v8-tracing-agent-impl.h" #include "runtime/Runtime.h" @@ -20,30 +23,31 @@ namespace v8_inspector { class WorkerInspectorClient; -class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { +// Debugs the main isolate and routes worker sessions. Any number of frontends +// may be connected; each gets its own V8 session on the main isolate and on +// every worker, and only sees replies to its own commands. Pauses are shared, +// as in Chrome: every frontend with the debugger enabled sees them, and any +// of them can resume. Isolate-wide V8 state is shared too: heap object +// tracking and sampling (any frontend's HeapProfiler.disable, or its +// disconnect, stops them for all) and the pause-on-exceptions setting (last +// writer wins). +class JsV8InspectorClient : V8InspectorClient { public: JsV8InspectorClient(tns::Runtime* runtime); void init(); void connect(int argc, char** argv); - void dispatchMessage(const std::string& message); // The single instance debugging the main isolate (created when IsDebug); // also acts as the router for worker sessions. Null in release builds. static JsV8InspectorClient* GetInstance(); - // Thread-safe write to the connected frontend (no-op when disconnected). - void SendToFrontend(const std::string& message); + // Thread-safe write to one frontend; dropped once it has disconnected. + void SendToConnection(uint64_t connectionId, const std::string& message); - // As SendToFrontend, but skips the source map rewrite (and the full-message - // scan and copy it costs) for payloads that cannot contain a sourceMapURL. - void SendRawToFrontend(const std::string& message); - - // Bumped, atomically with the sender swap, whenever a frontend goes away - // or is replaced. A worker session records it when created, reset or - // registered; output it sends with a stale generation belongs to a - // departed frontend and is dropped instead of reaching the next one. - uint64_t FrontendGeneration(); - void SendWorkerOutput(uint64_t generation, const std::string& message); + // As SendToConnection, but skips the source map rewrite (and the + // full-message scan and copy it costs) for payloads that cannot contain a + // sourceMapURL. + void SendRawToConnection(uint64_t connectionId, const std::string& message); // Worker targets (Chrome DevTools Target domain, flat-session mode). // Register/Unregister are called on the worker's own thread. @@ -52,11 +56,6 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { // Called on a worker thread by the Debugger.pause interrupt. void SchedulePauseInWorker(int workerId); - // Overrides of V8Inspector::Channel - void sendResponse(int callId, std::unique_ptr message) override; - void sendNotification(std::unique_ptr message) override; - void flushProtocolNotifications() override; - // Overrides of V8InspectorClient void runMessageLoopOnPause(int contextGroupId) override; void quitMessageLoopOnPause() override; @@ -72,49 +71,56 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { private: static constexpr int contextGroupId = 1; - // Queued on every connect and disconnect, ahead of anything the new - // frontend sends. When dispatched it replaces the main session if it served - // another connection, then binds the session's output to the current one. - static constexpr const char* kSessionSyncMessage = - "{\"__nsInternal\":\"syncSession\"}"; - - std::atomic isConnected_{false}; - // Set when a reset arrives inside the pause loop (which may run on any - // thread holding the Locker), where the session may still be on the stack; - // the next drain after the loop exits performs it. - std::atomic pendingSessionReset_{false}; + // Work for the main isolate, in arrival order. Connect and disconnect go + // through the same queue as messages so a frontend's session exists before + // its first message and outlives its last. + struct QueuedMessage { + enum class Kind { kConnect, kDisconnect, kMessage }; + uint64_t connectionId; + Kind kind; + std::string message; + }; + in_port_t listenPort_ = 0; // main queue only std::unique_ptr inspector_; v8::Persistent context_; - std::unique_ptr session_; + // Main isolate sessions, one per frontend. Touched by whichever thread is + // draining messages_ (the main thread, or the pause loop's thread); see + // InspectorSessions for the exceptions. + std::unique_ptr sessions_; tns::Runtime* runtime_; v8::Isolate* isolate_; bool terminated_ = false; - std::queue messages_; + std::queue messages_; bool runningNestedLoops_; dispatch_queue_t messagesQueue_; dispatch_queue_t messageLoopQueue_; dispatch_semaphore_t messageArrived_; - // The current connection; guarded by senderMutex_. - std::function sender_; - uint64_t connectionId_ = 0; - // The connection the main session serves, which lags the current one until - // the sync message is dispatched. Session output goes here, so replies to - // a replaced frontend's in-flight commands never reach its successor. - // Guarded by senderMutex_. - std::function sessionSender_; - uint64_t sessionConnectionId_ = 0; - uint64_t frontendGeneration_ = 0; // guarded by senderMutex_ bool isWaitingForDebugger_ = false; bool hasScheduledDebugBreak_ = false; + // Connected frontends' senders; guarded by frontendsMutex_, which is never + // held while taking another lock or calling a sender. + std::map> frontends_; + std::mutex frontendsMutex_; + + // Frontends that sent .enable to a JS domain handler (DOM, CSS, + // Network, ... from @nativescript/core). Those handlers are global, so a + // .disable only reaches them once no other frontend wants the + // domain. Only touched by whichever thread is draining messages_. + std::map> jsDomainEnablers_; + + // The process-global trace controller serves one frontend at a time. std::unique_ptr tracing_agent_; + uint64_t tracingOwner_ = 0; // guarded by tracingMutex_ + std::mutex tracingMutex_; // Streams backing Network.loadNetworkResource responses, read by the - // frontend through IO.read/IO.close (how Chrome DevTools fetches source - // maps from the target). Served on the socket thread for any session; - // guarded by resourceStreamsMutex_. + // frontend that opened them through IO.read/IO.close (how Chrome DevTools + // fetches source maps from the target). Served on the socket thread for + // any session; guarded by resourceStreamsMutex_. struct ResourceStream { + uint64_t connectionId; std::string data; size_t offset = 0; }; @@ -122,64 +128,57 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { int lastStreamId_ = 0; std::mutex resourceStreamsMutex_; - // Worker targets announced to the frontend via Target.attachedToTarget, - // keyed by their flat-protocol sessionId. Guarded by workerTargetsMutex_; - // a registered client pointer stays valid until UnregisterWorkerTarget - // (which runs on the worker's own thread, before the client is deleted). + // Worker targets, keyed by their flat-protocol sessionId. A registered + // client pointer stays valid until UnregisterWorkerTarget (which runs on + // the worker's own thread, before the client is deleted). struct WorkerTarget { int workerId; WorkerInspectorClient* client; - bool announced = false; }; + // Target domain state of one connected frontend. + struct TargetState { + bool autoAttach = false; + // sessionIds of the workers announced to this frontend. + std::set announced; + }; + // Both guarded by workerTargetsMutex_, which is taken before + // frontendsMutex_. A frontend has a TargetState exactly while it is + // connected, so nothing reaches a worker on its behalf after the worker + // was told it disconnected. std::map workerTargets_; + std::map targetStates_; std::mutex workerTargetsMutex_; - bool autoAttach_ = false; // guarded by workerTargetsMutex_ static JsV8InspectorClient* instance_; - std::mutex senderMutex_; - // Serializes onFrontendConnected/onFrontendDisconnected/ - // onFrontendMessageReceived, which run on different connection queues. - // Taken before senderMutex_, workerTargetsMutex_ and resourceStreamsMutex_, - // never while holding any of them. - std::mutex frontendLifecycleMutex_; - // Routes a frontend message carrying a sessionId to its worker session // (socket thread). msgId is -1 when the message has no id. - void RouteToWorker(const std::string& sessionId, const std::string& method, - long long msgId, const std::string& message); - // Announces all not-yet-announced workers (after Target.setAutoAttach). - void AnnounceWorkerTargets(); + void RouteToWorker(uint64_t connectionId, const std::string& sessionId, + const std::string& method, long long msgId, + const std::string& message); + // Announces every worker not yet announced to the frontend. Requires + // workerTargetsMutex_. + void AnnounceWorkerTargetsLocked(uint64_t connectionId, TargetState& state); + void SendAttachedToTarget(uint64_t connectionId, + WorkerInspectorClient* client); // Override of V8InspectorClient v8::Local ensureDefaultContextInGroup( int contextGroupId) override; void enableInspector(int argc, char** argv); - void createInspectorSession(); - void notify(std::unique_ptr message); - void notify(const std::string& message); + void SendToAll(const std::string& message); void onFrontendConnected(uint64_t connectionId, std::function sender); void onFrontendDisconnected(uint64_t connectionId); void onFrontendMessageReceived(uint64_t connectionId, const std::string& message); - // Socket thread: drops the state owned by the departing frontend that does - // not need the main isolate (IO streams, tracing, worker sessions). - void ResetFrontendState(); - // Pause loop only: resumes and defers the session swap to the next drain. - void DeferSessionResetFromPauseLoop(); - void ResetMainSession(); - // Binds the session to the current connection unless it served another - // one, in which case it binds nothing and returns false. - bool TryBindSession(); - // Never inside the pause loop: replaces the session if needed, then binds. - void SyncSessionWithFrontend(); - bool SessionOwnsFrontend(); - void QueueSessionSync(); - void EnqueueMessage(const std::string& message); + void EnqueueMessage(QueuedMessage message); void ScheduleMessageDrain(); - std::string PumpMessage(); + bool PumpMessage(QueuedMessage& message); + void HandleQueued(const QueuedMessage& message); + void DispatchFrontendMessage(uint64_t connectionId, + const std::string& message); static void registerDomainDispatcherCallback( const v8::FunctionCallbackInfo& args); static void inspectorSendEventCallback( @@ -191,12 +190,13 @@ class JsV8InspectorClient : V8InspectorClient, V8Inspector::Channel { // domain). V8's inspector doesn't implement these embedder domains. The // handlers are filesystem-only and thread-safe; they serve any session // (sessionId is echoed in the reply when non-empty). - void HandleLoadNetworkResource(int msgId, const std::string& url, + void HandleLoadNetworkResource(uint64_t connectionId, int msgId, + const std::string& url, const std::string& sessionId); - void HandleIORead(int msgId, const std::string& handle, int size, - const std::string& sessionId); - void HandleIOClose(int msgId, const std::string& handle, - const std::string& sessionId); + void HandleIORead(uint64_t connectionId, int msgId, const std::string& handle, + int size, const std::string& sessionId); + void HandleIOClose(uint64_t connectionId, int msgId, + const std::string& handle, const std::string& sessionId); // {N} specific helpers bool CallDomainHandlerFunction(v8::Local context, diff --git a/NativeScript/inspector/JsV8InspectorClient.mm b/NativeScript/inspector/JsV8InspectorClient.mm index 6d7fe775..8715a4fc 100644 --- a/NativeScript/inspector/JsV8InspectorClient.mm +++ b/NativeScript/inspector/JsV8InspectorClient.mm @@ -214,8 +214,8 @@ bool ShouldRewriteSourceMapURLs() { void JsV8InspectorClient::onFrontendConnected(uint64_t connectionId, std::function sender) { - std::lock_guard lifecycle(this->frontendLifecycleMutex_); - + // Called from the server's accept handler, which never runs concurrently + // with itself, so only the first frontend triggers the debug-brk pause. if (this->isWaitingForDebugger_) { this->isWaitingForDebugger_ = NO; CFRunLoopRef runloop = CFRunLoopGetMain(); @@ -226,75 +226,62 @@ bool ShouldRewriteSourceMapURLs() { CFRunLoopWakeUp(runloop); } - bool replacesFrontend; { - std::lock_guard lock(this->senderMutex_); - replacesFrontend = this->connectionId_ != 0; - if (replacesFrontend) { - ++this->frontendGeneration_; - } - this->sender_ = sender; - this->connectionId_ = connectionId; + std::lock_guard lock(this->frontendsMutex_); + this->frontends_[connectionId] = std::move(sender); } - - // The frontend's Debugger.enable must land on a fresh session so it replays - // Debugger.scriptParsed etc. The sync is ahead of anything this connection - // sends, since its reads only start after this returns. Without a previous - // frontend the disconnect (serialized with us by frontendLifecycleMutex_) - // already reset the frontend state (IO streams, tracing, worker sessions). - if (replacesFrontend) { - this->ResetFrontendState(); + { + std::lock_guard lock(this->workerTargetsMutex_); + this->targetStates_[connectionId] = TargetState(); } - this->QueueSessionSync(); - this->isConnected_ = true; + + this->EnqueueMessage({connectionId, QueuedMessage::Kind::kConnect, std::string()}); + this->ScheduleMessageDrain(); } void JsV8InspectorClient::onFrontendDisconnected(uint64_t connectionId) { - std::lock_guard lifecycle(this->frontendLifecycleMutex_); + // Workers drop the frontend's sessions; their remaining output for it is + // dropped by SendToConnection. + { + std::lock_guard lock(this->workerTargetsMutex_); + this->targetStates_.erase(connectionId); + for (auto& entry : this->workerTargets_) { + entry.second.client->PushDisconnect(connectionId); + } + } + { + std::lock_guard lock(this->frontendsMutex_); + this->frontends_.erase(connectionId); + } + // Resource stream handles only have meaning to the frontend that opened + // them, so drop any streams it never closed via IO.close. { - std::lock_guard lock(this->senderMutex_); - // A newer connection already took over. - if (this->connectionId_ != connectionId) { - return; + std::lock_guard lock(this->resourceStreamsMutex_); + for (auto it = this->resourceStreams_.begin(); it != this->resourceStreams_.end();) { + if (it->second.connectionId == connectionId) { + it = this->resourceStreams_.erase(it); + } else { + ++it; + } } - ++this->frontendGeneration_; - this->sender_ = nullptr; - this->connectionId_ = 0; } - // Nobody is left to resume a pause or read what the session emits. - this->isConnected_ = false; - this->ResetFrontendState(); - this->QueueSessionSync(); -} + // Free the process-global trace controller for other frontends. + { + std::lock_guard lock(this->tracingMutex_); + if (this->tracingOwner_ == connectionId) { + this->tracingOwner_ = 0; + tracing_agent_->stopAndDiscard(); + } + } -void JsV8InspectorClient::QueueSessionSync() { - // Everything still queued came from a departed frontend (or is an earlier - // sync, which this one supersedes). Dropping it keeps its replies, whose - // ids may collide with the next frontend's, from reaching that frontend. - dispatch_sync(this->messagesQueue_, ^{ - this->messages_ = {}; - this->messages_.push(kSessionSyncMessage); - dispatch_semaphore_signal(messageArrived_); - }); + this->EnqueueMessage({connectionId, QueuedMessage::Kind::kDisconnect, std::string()}); this->ScheduleMessageDrain(); } void JsV8InspectorClient::onFrontendMessageReceived(uint64_t connectionId, const std::string& message) { - // Held through processing so a takeover cannot run between the check and - // the enqueue: its reset then drops anything this queued. - std::lock_guard lifecycle(this->frontendLifecycleMutex_); - { - std::lock_guard lock(this->senderMutex_); - // Read before its connection was replaced; the reply would reach the new - // frontend. - if (this->connectionId_ != connectionId) { - return; - } - } - // Single parse, on the socket thread, used for routing and fast paths. auto parsed = json::parse(message, nullptr, false); std::string sessionId; @@ -321,7 +308,7 @@ bool ShouldRewriteSourceMapURLs() { if (parsed.contains("params") && parsed["params"].contains("url")) { url = parsed["params"]["url"].get(); } - this->HandleLoadNetworkResource(static_cast(msgId), url, sessionId); + this->HandleLoadNetworkResource(connectionId, static_cast(msgId), url, sessionId); return; } @@ -339,9 +326,9 @@ bool ShouldRewriteSourceMapURLs() { } if (method == "IO.read") { - this->HandleIORead(static_cast(msgId), handle, size, sessionId); + this->HandleIORead(connectionId, static_cast(msgId), handle, size, sessionId); } else { - this->HandleIOClose(static_cast(msgId), handle, sessionId); + this->HandleIOClose(connectionId, static_cast(msgId), handle, sessionId); } return; } @@ -351,7 +338,8 @@ bool ShouldRewriteSourceMapURLs() { // frontend must not wait for (or block) JS. The dataCollected chunks are // pre-serialized by the trace writer and cannot carry a sessionId without // re-parsing them, so only the main session is served; V8 traces the whole - // process anyway. + // process anyway. The controller is process-global too, so one frontend + // owns it from Tracing.start until its Tracing.end or disconnect. if (sessionId.empty() && method == "Tracing.start") { std::vector categories; double bufferSizeInKb = 0; @@ -384,52 +372,64 @@ bool ShouldRewriteSourceMapURLs() { } json reply = {{"id", msgId}}; - if (tracing_agent_->start(categories, bufferSizeInKb)) { - reply["result"] = json::object(); - } else { - reply["error"] = {{"code", -32000}, {"message", "Tracing is already started"}}; + { + std::lock_guard lock(this->tracingMutex_); + if (this->tracingOwner_ == 0 && tracing_agent_->start(categories, bufferSizeInKb)) { + this->tracingOwner_ = connectionId; + reply["result"] = json::object(); + } else { + reply["error"] = {{"code", -32000}, {"message", "Tracing is already started"}}; + } } - this->SendRawToFrontend(reply.dump()); + this->SendRawToConnection(connectionId, reply.dump()); return; } if (sessionId.empty() && method == "Tracing.end") { tns::inspector::TracingAgentImpl::Result trace; - if (!tracing_agent_->end(trace)) { + bool ended; + { + std::lock_guard lock(this->tracingMutex_); + ended = this->tracingOwner_ == connectionId && tracing_agent_->end(trace); + if (ended) { + this->tracingOwner_ = 0; + } + } + if (!ended) { json error = {{"id", msgId}, {"error", {{"code", -32000}, {"message", "Tracing is not started"}}}}; - this->SendRawToFrontend(error.dump()); + this->SendRawToConnection(connectionId, error.dump()); return; } // The ack must reach the frontend before the events it asked for. json ack = {{"id", msgId}, {"result", json::object()}}; - this->SendRawToFrontend(ack.dump()); + this->SendRawToConnection(connectionId, ack.dump()); for (const auto& traceMessage : trace.messages) { - this->SendRawToFrontend(traceMessage); + this->SendRawToConnection(connectionId, traceMessage); } json complete = {{"method", "Tracing.tracingComplete"}, {"params", {{"dataLossOccurred", trace.dataLossOccurred}}}}; - this->SendRawToFrontend(complete.dump()); + this->SendRawToConnection(connectionId, complete.dump()); return; } // Messages carrying a sessionId belong to a worker target (flat-session // protocol); route them to the worker's own thread. if (!sessionId.empty()) { - this->RouteToWorker(sessionId, method, msgId, message); + this->RouteToWorker(connectionId, sessionId, method, msgId, message); return; } - this->EnqueueMessage(message); + this->EnqueueMessage({connectionId, QueuedMessage::Kind::kMessage, message}); // Debugger.pause needs to interrupt V8 even if the main thread is busy // executing JS. RequestInterrupt fires at the next safe bytecode boundary. if (method == "Debugger.pause") { + sessions_->RequestPause(connectionId); isolate_->RequestInterrupt( [](Isolate* isolate, void* data) { - auto client = static_cast(data); - client->session_->schedulePauseOnNextStatement({}, {}); + static_cast(data)->sessions_->TakePauseRequests(); }, this); } @@ -437,43 +437,62 @@ bool ShouldRewriteSourceMapURLs() { this->ScheduleMessageDrain(); } -void JsV8InspectorClient::EnqueueMessage(const std::string& message) { +void JsV8InspectorClient::EnqueueMessage(QueuedMessage message) { + __block QueuedMessage queued = std::move(message); dispatch_sync(this->messagesQueue_, ^{ - this->messages_.push(message); + this->messages_.push(std::move(queued)); dispatch_semaphore_signal(messageArrived_); }); } void JsV8InspectorClient::ScheduleMessageDrain() { tns::ExecuteOnMainThread([this]() { + // A running pause loop pumps the queue itself, and schedules a drain once + // it has exited for anything that arrives after its last pump. + __block bool pauseLoopRunning = false; dispatch_sync(this->messageLoopQueue_, ^{ - // prevent execution if we're already pumping messages - if (runningNestedLoops_ && !terminated_) { - return; - }; - if (this->pendingSessionReset_) { - // A pause loop (possibly on another thread) is still winding down; - // it schedules another drain once it has exited. - if (runningNestedLoops_) { - return; - } - this->pendingSessionReset_ = false; - this->SyncSessionWithFrontend(); - } - std::string message; - do { - message = this->PumpMessage(); - if (!message.empty()) { - this->dispatchMessage(message); - } - } while (!message.empty()); + pauseLoopRunning = runningNestedLoops_; }); + if (pauseLoopRunning) { + return; + } + + // Not under messageLoopQueue_: a message can pause right here, and the + // pause loop needs that queue. + QueuedMessage message; + while (this->PumpMessage(message)) { + this->HandleQueued(message); + } }); } -void JsV8InspectorClient::RouteToWorker(const std::string& sessionId, const std::string& method, - long long msgId, const std::string& message) { +void JsV8InspectorClient::HandleQueued(const QueuedMessage& message) { + switch (message.kind) { + case QueuedMessage::Kind::kConnect: + sessions_->Connect(inspector_.get(), message.connectionId); + break; + case QueuedMessage::Kind::kDisconnect: + for (auto& entry : this->jsDomainEnablers_) { + entry.second.erase(message.connectionId); + } + sessions_->Disconnect(message.connectionId); + break; + case QueuedMessage::Kind::kMessage: + this->DispatchFrontendMessage(message.connectionId, message.message); + break; + } +} + +void JsV8InspectorClient::RouteToWorker(uint64_t connectionId, const std::string& sessionId, + const std::string& method, long long msgId, + const std::string& message) { std::lock_guard lock(this->workerTargetsMutex_); + // The worker was already told this frontend disconnected; a message now + // would recreate its session. + if (this->targetStates_.count(connectionId) == 0) { + return; + } + auto it = this->workerTargets_.find(sessionId); if (it == this->workerTargets_.end()) { // The worker died (Target.detachedFromTarget was sent) or never existed. @@ -481,7 +500,7 @@ bool ShouldRewriteSourceMapURLs() { json error = {{"id", msgId}, {"sessionId", sessionId}, {"error", {{"code", -32001}, {"message", "Session not found"}}}}; - this->SendToFrontend(error.dump()); + this->SendRawToConnection(connectionId, error.dump()); } return; } @@ -492,10 +511,10 @@ bool ShouldRewriteSourceMapURLs() { // executing JS. The worker isolate is guaranteed alive while we hold the // registry lock (teardown unregisters before disposing it). if (method == "Debugger.pause") { - client->RequestPauseInterrupt(); + client->RequestPauseInterrupt(connectionId); } - client->PushMessage(message); + client->PushMessage(connectionId, message); } void JsV8InspectorClient::init() { @@ -517,107 +536,18 @@ bool ShouldRewriteSourceMapURLs() { context_.Reset(isolate, context); - this->createInspectorSession(); + sessions_ = std::make_unique( + isolate, JsV8InspectorClient::contextGroupId, + [this](uint64_t connectionId, const std::string& message) { + this->SendToConnection(connectionId, message); + }); tracing_agent_.reset(new tns::inspector::TracingAgentImpl()); } -void JsV8InspectorClient::connect(int argc, char** argv) { - this->isConnected_ = true; - this->enableInspector(argc, argv); -} - -void JsV8InspectorClient::createInspectorSession() { - this->session_ = this->inspector_->connect(JsV8InspectorClient::contextGroupId, this, {}, - v8_inspector::V8Inspector::kFullyTrusted); -} - -void JsV8InspectorClient::ResetFrontendState() { - // Resource stream handles only have meaning to the frontend that opened - // them, so drop any streams it never closed via IO.close. - { - std::lock_guard lock(this->resourceStreamsMutex_); - this->resourceStreams_.clear(); - } - - // Drop any trace the departing frontend left running so the process-global - // controller is free for the next frontend's Tracing.start. - tracing_agent_->stopAndDiscard(); - - // Reset worker sessions too: resume any paused worker and recreate its - // session so the (re)connecting frontend gets a clean slate, then forget - // the auto-attach state until it sends Target.setAutoAttach again. - { - std::lock_guard lock(this->workerTargetsMutex_); - this->autoAttach_ = false; - for (auto& entry : this->workerTargets_) { - entry.second.announced = false; - entry.second.client->PushMessage(WorkerInspectorClient::kResetSessionMessage); - } - } -} - -void JsV8InspectorClient::DeferSessionResetFromPauseLoop() { - // The pause loop may itself run under session_->dispatchProtocolMessage, - // and V8 touches every session right after the loop returns, so the swap - // must wait until the stack has unwound. Resume now and keep the old - // session from pausing again in the meantime. - this->pendingSessionReset_ = true; - Isolate* isolate = isolate_; - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - session_->setSkipAllPauses(true); - session_->resume(); -} - -void JsV8InspectorClient::ResetMainSession() { - Isolate* isolate = isolate_; - v8::Locker locker(isolate); - Isolate::Scope isolate_scope(isolate); - HandleScope handle_scope(isolate); - - session_->resume(); - session_.reset(); - - this->createInspectorSession(); -} - -bool JsV8InspectorClient::TryBindSession() { - std::lock_guard lock(this->senderMutex_); - if (this->sessionConnectionId_ != 0 && this->sessionConnectionId_ != this->connectionId_) { - return false; - } - this->sessionConnectionId_ = this->connectionId_; - this->sessionSender_ = this->sender_; - return true; -} - -void JsV8InspectorClient::SyncSessionWithFrontend() { - if (this->TryBindSession()) { - return; - } - - this->ResetMainSession(); - { - std::lock_guard lock(this->senderMutex_); - this->sessionConnectionId_ = 0; - this->sessionSender_ = nullptr; - } - this->TryBindSession(); -} - -bool JsV8InspectorClient::SessionOwnsFrontend() { - std::lock_guard lock(this->senderMutex_); - return this->sessionConnectionId_ != 0 && this->sessionConnectionId_ == this->connectionId_; -} +void JsV8InspectorClient::connect(int argc, char** argv) { this->enableInspector(argc, argv); } void JsV8InspectorClient::runMessageLoopOnPause(int contextGroupId) { - // The session that paused is about to be replaced; no frontend owns it. - if (this->pendingSessionReset_) { - return; - } - __block auto loopsRunning = false; dispatch_sync(this->messageLoopQueue_, ^{ loopsRunning = runningNestedLoops_; @@ -631,18 +561,15 @@ bool ShouldRewriteSourceMapURLs() { if (loopsRunning) { return; } + sessions_->ClearPauseRequests(); bool shouldWait = false; while (!terminated_) { - std::string message = this->PumpMessage(); - if (message == kSessionSyncMessage) { - if (!this->TryBindSession()) { - this->DeferSessionResetFromPauseLoop(); - break; - } - shouldWait = false; - } else if (!message.empty()) { - this->dispatchMessage(message); + QueuedMessage message; + if (this->PumpMessage(message)) { + // A disconnect here may end the pause: once no frontend has the + // debugger enabled, V8 calls quitMessageLoopOnPause. + this->HandleQueued(message); shouldWait = false; } else { shouldWait = true; @@ -666,10 +593,7 @@ bool ShouldRewriteSourceMapURLs() { terminated_ = false; runningNestedLoops_ = false; }); - - if (this->pendingSessionReset_) { - this->ScheduleMessageDrain(); - } + this->ScheduleMessageDrain(); } void JsV8InspectorClient::quitMessageLoopOnPause() { @@ -678,80 +602,54 @@ bool ShouldRewriteSourceMapURLs() { }); } -void JsV8InspectorClient::sendResponse(int callId, std::unique_ptr message) { - this->notify(std::move(message)); -} - -void JsV8InspectorClient::sendNotification(std::unique_ptr message) { - this->notify(std::move(message)); -} - -void JsV8InspectorClient::flushProtocolNotifications() {} - -void JsV8InspectorClient::notify(std::unique_ptr message) { - StringView stringView = message->string(); - notify(ToStdString(stringView)); -} - -void JsV8InspectorClient::notify(const std::string& message) { - std::function sender; - { - std::lock_guard lock(this->senderMutex_); - sender = this->sessionSender_; - } - if (sender) { - sender(MaybeRewriteSourceMapURL(message)); - } -} - -uint64_t JsV8InspectorClient::FrontendGeneration() { - std::lock_guard lock(this->senderMutex_); - return this->frontendGeneration_; +void JsV8InspectorClient::SendToConnection(uint64_t connectionId, const std::string& message) { + this->SendRawToConnection(connectionId, MaybeRewriteSourceMapURL(message)); } -void JsV8InspectorClient::SendWorkerOutput(uint64_t generation, const std::string& message) { +void JsV8InspectorClient::SendRawToConnection(uint64_t connectionId, const std::string& message) { std::function sender; { - std::lock_guard lock(this->senderMutex_); - if (generation != this->frontendGeneration_) { + std::lock_guard lock(this->frontendsMutex_); + auto it = this->frontends_.find(connectionId); + if (it == this->frontends_.end()) { return; } - sender = this->sender_; + sender = it->second; } - if (sender) { - sender(MaybeRewriteSourceMapURL(message)); - } -} - -void JsV8InspectorClient::SendToFrontend(const std::string& message) { - this->SendRawToFrontend(MaybeRewriteSourceMapURL(message)); + sender(message); } -void JsV8InspectorClient::SendRawToFrontend(const std::string& message) { - std::function sender; +void JsV8InspectorClient::SendToAll(const std::string& message) { + std::vector> senders; { - std::lock_guard lock(this->senderMutex_); - sender = this->sender_; + std::lock_guard lock(this->frontendsMutex_); + for (const auto& entry : this->frontends_) { + senders.push_back(entry.second); + } } - if (sender) { + for (const auto& sender : senders) { sender(message); } } -void JsV8InspectorClient::dispatchMessage(const std::string& message) { - // The pause loop intercepts this message, so here no session is on the stack. - if (message == kSessionSyncMessage) { - this->SyncSessionWithFrontend(); +void JsV8InspectorClient::DispatchFrontendMessage(uint64_t connectionId, + const std::string& message) { + // The frontend disconnected (or is disconnecting) after sending this. + if (!sessions_->Has(connectionId)) { return; } - auto json_message = json::parse(message); + auto json_message = json::parse(message, nullptr, false); + if (json_message.is_discarded() || !json_message.is_object() || + !json_message.contains("method") || !json_message["method"].is_string()) { + NSLog(@"Inspector failed to parse incoming message: %s", message.c_str()); + return; + } std::string method = json_message["method"]; // Note: the Tracing domain is handled in onFrontendMessageReceived, so it // never waits on the message queue or takes the Locker below. - StringView messageView = Make8BitStringView(message); Isolate* isolate = isolate_; v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); @@ -784,23 +682,17 @@ bool ShouldRewriteSourceMapURLs() { bool autoAttach = json_message.contains("params") && json_message["params"].contains("autoAttach") && json_message["params"]["autoAttach"].get(); - { - std::lock_guard lock(this->workerTargetsMutex_); - // A replaced frontend's in-flight request must not turn auto-attach on - // for its successor. Checked under the lock ResetFrontendState takes - // after the sender swap, so a stale value set here is reset there. - if (!this->SessionOwnsFrontend()) { - autoAttach = false; - } else { - this->autoAttach_ = autoAttach; - } - } json response = {{"id", json_message["id"]}, {"result", json::object()}}; - this->notify(response.dump()); + this->SendRawToConnection(connectionId, response.dump()); - if (autoAttach) { - this->AnnounceWorkerTargets(); + std::lock_guard lock(this->workerTargetsMutex_); + auto it = this->targetStates_.find(connectionId); + if (it != this->targetStates_.end()) { + it->second.autoAttach = autoAttach; + if (autoAttach) { + this->AnnounceWorkerTargetsLocked(connectionId, it->second); + } } return; } @@ -810,7 +702,7 @@ bool ShouldRewriteSourceMapURLs() { if (method == "Target.setDiscoverTargets" || method == "Target.setRemoteLocations" || method == "Target.detachFromTarget") { json response = {{"id", json_message["id"]}, {"result", json::object()}}; - this->notify(response.dump()); + this->SendRawToConnection(connectionId, response.dump()); return; } @@ -832,37 +724,43 @@ bool ShouldRewriteSourceMapURLs() { Local domainMethodFunc = v8_inspector::GetDebuggerFunctionFromObject(context, argObject, domainDebugger); + size_t dot = method.find('.'); + std::string domain = method.substr(0, dot); + std::string command = dot == std::string::npos ? std::string() : method.substr(dot + 1); + bool hasHandler = !domainMethodFunc.IsEmpty() && domainMethodFunc->IsFunction(); + + if (hasHandler && command == "disable") { + std::set& enablers = this->jsDomainEnablers_[domain]; + enablers.erase(connectionId); + if (!enablers.empty()) { + json response = {{"id", json_message["id"]}, {"result", json::object()}}; + this->SendRawToConnection(connectionId, response.dump()); + return; + } + } + Local result; success = this->CallDomainHandlerFunction(context, domainMethodFunc, argObject, domainDebugger, result); if (success) { + if (command == "enable") { + this->jsDomainEnablers_[domain].insert(connectionId); + } + auto requestId = arg.As()->Get(context, tns::ToV8String(isolate, "id")).ToLocalChecked(); auto returnString = GetReturnMessageFromDomainHandlerResult(result, requestId); if (returnString.size() > 0) { - StringView messageView = Make8BitStringView(returnString); - auto msg = StringBuffer::create(messageView); - this->sendNotification(std::move(msg)); + this->SendToConnection(connectionId, returnString); } return; } } // if no handler handled the message successfully, fall-through to the default V8 implementation - this->session_->dispatchProtocolMessage(messageView); - // if needed to test, disable the default handling and use this - // json error = { - // {"id", json_message["id"]}, - // {"error", { - // {"code", -32601}, - // { - // "message", "Method not found: " + method - // } - // }} - // }; - // notify(error.dump()); + sessions_->Dispatch(connectionId, message); // TODO: check why this is needed (it should trigger automatically when script depth is 0) isolate->PerformMicrotaskCheckpoint(); @@ -879,7 +777,8 @@ json WithSessionId(json message, const std::string& sessionId) { } } // namespace -void JsV8InspectorClient::HandleLoadNetworkResource(int msgId, const std::string& url, +void JsV8InspectorClient::HandleLoadNetworkResource(uint64_t connectionId, int msgId, + const std::string& url, const std::string& sessionId) { std::string path; if (url.rfind(kSourceMapScheme, 0) == 0) { @@ -892,7 +791,7 @@ json WithSessionId(json message, const std::string& sessionId) { // pre-existing behavior for http(s) urls. json error = {{"id", msgId}, {"error", {{"code", -32000}, {"message", "Unsupported URL scheme"}}}}; - this->SendToFrontend(WithSessionId(error, sessionId).dump()); + this->SendToConnection(connectionId, WithSessionId(error, sessionId).dump()); return; } @@ -935,7 +834,7 @@ json WithSessionId(json message, const std::string& sessionId) { { std::lock_guard lock(this->resourceStreamsMutex_); handle = "ns-network-resource-" + std::to_string(++lastStreamId_); - resourceStreams_[handle] = {std::move(content), 0}; + resourceStreams_[handle] = {connectionId, std::move(content), 0}; } resource = {{"success", true}, {"httpStatusCode", 200}, {"stream", handle}}; } else { @@ -946,19 +845,19 @@ json WithSessionId(json message, const std::string& sessionId) { } json response = {{"id", msgId}, {"result", {{"resource", resource}}}}; - this->SendToFrontend(WithSessionId(response, sessionId).dump()); + this->SendToConnection(connectionId, WithSessionId(response, sessionId).dump()); } -void JsV8InspectorClient::HandleIORead(int msgId, const std::string& handle, int size, - const std::string& sessionId) { +void JsV8InspectorClient::HandleIORead(uint64_t connectionId, int msgId, const std::string& handle, + int size, const std::string& sessionId) { json result; { std::lock_guard lock(this->resourceStreamsMutex_); auto it = resourceStreams_.find(handle); - if (it == resourceStreams_.end()) { + if (it == resourceStreams_.end() || it->second.connectionId != connectionId) { json error = {{"id", msgId}, {"error", {{"code", -32602}, {"message", "Invalid stream handle"}}}}; - this->SendToFrontend(WithSessionId(error, sessionId).dump()); + this->SendToConnection(connectionId, WithSessionId(error, sessionId).dump()); return; } @@ -982,44 +881,48 @@ json WithSessionId(json message, const std::string& sessionId) { } json response = {{"id", msgId}, {"result", result}}; - this->SendToFrontend(WithSessionId(response, sessionId).dump()); + this->SendToConnection(connectionId, WithSessionId(response, sessionId).dump()); } -void JsV8InspectorClient::HandleIOClose(int msgId, const std::string& handle, +void JsV8InspectorClient::HandleIOClose(uint64_t connectionId, int msgId, const std::string& handle, const std::string& sessionId) { { std::lock_guard lock(this->resourceStreamsMutex_); - resourceStreams_.erase(handle); + auto it = resourceStreams_.find(handle); + if (it != resourceStreams_.end() && it->second.connectionId == connectionId) { + resourceStreams_.erase(it); + } } json response = {{"id", msgId}, {"result", json::object()}}; - this->SendToFrontend(WithSessionId(response, sessionId).dump()); + this->SendToConnection(connectionId, WithSessionId(response, sessionId).dump()); +} + +void JsV8InspectorClient::SendAttachedToTarget(uint64_t connectionId, + WorkerInspectorClient* client) { + json attached = {{"method", "Target.attachedToTarget"}, + {"params", + {{"sessionId", client->SessionId()}, + {"targetInfo", + {{"targetId", client->TargetId()}, + {"type", "worker"}, + {"title", client->Url()}, + {"url", client->Url()}, + {"attached", true}, + {"canAccessOpener", false}}}, + {"waitingForDebugger", false}}}}; + this->SendRawToConnection(connectionId, attached.dump()); } void JsV8InspectorClient::RegisterWorkerTarget(int workerId, WorkerInspectorClient* client) { std::lock_guard lock(this->workerTargetsMutex_); - WorkerTarget target{workerId, client, false}; - - // Under the lock ResetFrontendState takes after bumping the generation: - // either this sees the bumped value or the reset sweep sees this worker. - client->SetFrontendGeneration(this->FrontendGeneration()); - - if (this->isConnected_ && this->autoAttach_) { - target.announced = true; - json attached = {{"method", "Target.attachedToTarget"}, - {"params", - {{"sessionId", client->SessionId()}, - {"targetInfo", - {{"targetId", client->TargetId()}, - {"type", "worker"}, - {"title", client->Url()}, - {"url", client->Url()}, - {"attached", true}, - {"canAccessOpener", false}}}, - {"waitingForDebugger", false}}}}; - this->notify(attached.dump()); - } + this->workerTargets_.emplace(client->SessionId(), WorkerTarget{workerId, client}); - this->workerTargets_.emplace(client->SessionId(), target); + for (auto& entry : this->targetStates_) { + if (entry.second.autoAttach) { + entry.second.announced.insert(client->SessionId()); + this->SendAttachedToTarget(entry.first, client); + } + } } void JsV8InspectorClient::UnregisterWorkerTarget(int workerId) { @@ -1029,12 +932,15 @@ json WithSessionId(json message, const std::string& sessionId) { continue; } - if (it->second.announced && this->isConnected_) { - json detached = {{"method", "Target.detachedFromTarget"}, - {"params", - {{"sessionId", it->second.client->SessionId()}, - {"targetId", it->second.client->TargetId()}}}}; - this->notify(detached.dump()); + const std::string& sessionId = it->second.client->SessionId(); + json detached = { + {"method", "Target.detachedFromTarget"}, + {"params", {{"sessionId", sessionId}, {"targetId", it->second.client->TargetId()}}}}; + std::string detachedMessage = detached.dump(); + for (auto& entry : this->targetStates_) { + if (entry.second.announced.erase(sessionId) != 0) { + this->SendRawToConnection(entry.first, detachedMessage); + } } this->workerTargets_.erase(it); @@ -1042,32 +948,11 @@ json WithSessionId(json message, const std::string& sessionId) { } } -void JsV8InspectorClient::AnnounceWorkerTargets() { - std::lock_guard lock(this->workerTargetsMutex_); - // A frontend change since Target.setAutoAttach cleared this; marking - // workers announced now would hide them from the next frontend. - if (!this->autoAttach_) { - return; - } +void JsV8InspectorClient::AnnounceWorkerTargetsLocked(uint64_t connectionId, TargetState& state) { for (auto& entry : this->workerTargets_) { - WorkerTarget& target = entry.second; - if (target.announced) { - continue; + if (state.announced.insert(entry.first).second) { + this->SendAttachedToTarget(connectionId, entry.second.client); } - target.announced = true; - - json attached = {{"method", "Target.attachedToTarget"}, - {"params", - {{"sessionId", target.client->SessionId()}, - {"targetInfo", - {{"targetId", target.client->TargetId()}, - {"type", "worker"}, - {"title", target.client->Url()}, - {"url", target.client->Url()}, - {"attached", true}, - {"canAccessOpener", false}}}, - {"waitingForDebugger", false}}}}; - this->notify(attached.dump()); } } @@ -1085,16 +970,21 @@ json WithSessionId(json message, const std::string& sessionId) { return context_.Get(isolate_); } -std::string JsV8InspectorClient::PumpMessage() { - __block std::string result; +bool JsV8InspectorClient::PumpMessage(QueuedMessage& message) { + __block bool found = false; + __block QueuedMessage result; dispatch_sync(this->messagesQueue_, ^{ - if (this->messages_.size() > 0) { - result = this->messages_.front(); + if (!this->messages_.empty()) { + result = std::move(this->messages_.front()); this->messages_.pop(); + found = true; } }); - return result; + if (found) { + message = std::move(result); + } + return found; } void JsV8InspectorClient::scheduleBreak() { @@ -1116,7 +1006,7 @@ json WithSessionId(json message, const std::string& sessionId) { .ToChecked(); } - this->session_->schedulePauseOnNextStatement({}, {}); + sessions_->SchedulePauseInAll(); } void JsV8InspectorClient::registerModules() { @@ -1203,9 +1093,9 @@ json WithSessionId(json message, const std::string& sessionId) { Local arg = args[0].As(); std::string message = tns::ToString(isolate, arg); - StringView messageView = Make8BitStringView(message); - auto msg = StringBuffer::create(messageView); - client->sendNotification(std::move(msg)); + // The JS domains are shared by every frontend and emit events even before + // any of them sent .enable, so every frontend gets them. + client->SendToAll(message); } void JsV8InspectorClient::inspectorTimestampCallback(const FunctionCallbackInfo& args) { diff --git a/NativeScript/inspector/WorkerInspectorClient.h b/NativeScript/inspector/WorkerInspectorClient.h index e635ada7..4426dce7 100644 --- a/NativeScript/inspector/WorkerInspectorClient.h +++ b/NativeScript/inspector/WorkerInspectorClient.h @@ -6,11 +6,13 @@ #include #include +#include #include #include #include #include +#include "InspectorSessions.h" #include "include/v8-inspector.h" #include "src/inspector/v8-console-message.h" @@ -18,17 +20,17 @@ namespace v8_inspector { // V8 inspector for a single worker isolate, exposed to Chrome DevTools as a // child target ("Target.attachedToTarget") and addressed with flat-session -// CDP messages (a top-level "sessionId" field). One instance per worker. +// CDP messages (a top-level "sessionId" field). One instance per worker, with +// one V8 session per frontend that talks to it. // // Threading: constructed, dispatched into, and destroyed on the worker's own // thread (V8's inspector is not thread-safe). Other threads interact only -// through PushMessage/NotifyTerminating/RequestPauseInterrupt. Incoming -// messages are queued and drained via a CFRunLoopSource on the worker's -// runloop; while paused, a nested loop on the worker thread pumps the same -// queue (the runloop is NOT re-entered, so postMessage deliveries stay queued -// during a pause, matching Chrome's semantics). -class WorkerInspectorClient final : public V8InspectorClient, - public V8Inspector::Channel { +// through PushMessage/PushDisconnect/NotifyTerminating/RequestPauseInterrupt. +// Incoming messages are queued and drained via a CFRunLoopSource on the +// worker's runloop; while paused, a nested loop on the worker thread pumps the +// same queue (the runloop is NOT re-entered, so postMessage deliveries stay +// queued during a pause, matching Chrome's semantics). +class WorkerInspectorClient final : public V8InspectorClient { public: // Worker thread, with the worker isolate locked and its context created. WorkerInspectorClient(int workerId, v8::Isolate* isolate, @@ -40,14 +42,13 @@ class WorkerInspectorClient final : public V8InspectorClient, const std::string& TargetId() const { return targetId_; } const std::string& Url() const { return url_; } - // Worker thread. See JsV8InspectorClient::FrontendGeneration. - void SetFrontendGeneration(uint64_t generation) { - frontendGeneration_ = generation; - } + // Any thread. Queues a CDP message from a frontend and wakes the worker + // runloop / a nested pause loop. The frontend's first message creates its + // session, so callers must stop pushing once they pushed its disconnect. + void PushMessage(uint64_t connectionId, const std::string& message); - // Any thread. Queues a CDP message (already stripped of routing concerns) - // and wakes the worker runloop / a nested pause loop. - void PushMessage(const std::string& message); + // Any thread. Drops the frontend's session. + void PushDisconnect(uint64_t connectionId); // Any thread. Unblocks a paused worker and makes it drop all inspector // work; used by WorkerWrapper::Terminate together with TerminateExecution. @@ -55,7 +56,7 @@ class WorkerInspectorClient final : public V8InspectorClient, // Any thread (with the worker isolate guaranteed alive). Schedules a pause // at the next statement even if the worker is busy executing JS. - void RequestPauseInterrupt(); + void RequestPauseInterrupt(uint64_t connectionId); // Worker thread (from the interrupt requested above). void SchedulePauseFromInterrupt(); @@ -64,16 +65,6 @@ class WorkerInspectorClient final : public V8InspectorClient, void consoleLog(ConsoleAPIType method, const std::vector>& args); - // Internal control message pushed by the root client on frontend - // reconnect; resumes the worker if paused and recreates its session. - static constexpr const char* kResetSessionMessage = - "{\"__nsInternal\":\"resetSession\"}"; - - // Overrides of V8Inspector::Channel - void sendResponse(int callId, std::unique_ptr message) override; - void sendNotification(std::unique_ptr message) override; - void flushProtocolNotifications() override; - // Overrides of V8InspectorClient void runMessageLoopOnPause(int contextGroupId) override; void quitMessageLoopOnPause() override; @@ -81,13 +72,17 @@ class WorkerInspectorClient final : public V8InspectorClient, private: static constexpr int contextGroupId = 1; + struct Incoming { + uint64_t connectionId; + bool disconnect; + std::string message; + }; + void DrainIncoming(); - std::string PopMessage(); - void DispatchOne(const std::string& message); - void HandleResetRequest(); - void DoResetSession(); - void MaybeResetSession(); - void SendWrapped(const std::string& message); + bool PopIncoming(Incoming& incoming); + void PushIncoming(Incoming incoming); + void HandleIncoming(const Incoming& incoming); + void SendWrapped(uint64_t connectionId, const std::string& message); v8::Local ensureDefaultContextInGroup( int contextGroupId) override; @@ -101,20 +96,16 @@ class WorkerInspectorClient final : public V8InspectorClient, CFRunLoopSourceRef source_ = nullptr; std::unique_ptr inspector_; - std::unique_ptr session_; + std::unique_ptr sessions_; v8::Persistent context_; - std::queue incoming_; + std::queue incoming_; std::mutex incomingMutex_; dispatch_semaphore_t messageArrived_; std::atomic dying_{false}; std::atomic pauseTerminated_{false}; bool runningPauseLoop_ = false; // worker thread only - bool pendingReset_ = false; // worker thread only - // JsV8InspectorClient::FrontendGeneration() as of this session's latest - // (re)creation or registration; worker thread only. - uint64_t frontendGeneration_ = 0; }; } // namespace v8_inspector diff --git a/NativeScript/inspector/WorkerInspectorClient.mm b/NativeScript/inspector/WorkerInspectorClient.mm index 0107a1fc..9b5cfdf1 100644 --- a/NativeScript/inspector/WorkerInspectorClient.mm +++ b/NativeScript/inspector/WorkerInspectorClient.mm @@ -57,8 +57,11 @@ StringView Make8BitStringView(const std::string& value) { contextInfo.origin = Make8BitStringView(url_); inspector_->contextCreated(contextInfo); context_.Reset(isolate_, context); - session_ = - inspector_->connect(contextGroupId, this, {}, v8_inspector::V8Inspector::kFullyTrusted); + + sessions_ = std::make_unique( + isolate_, contextGroupId, [this](uint64_t connectionId, const std::string& message) { + this->SendWrapped(connectionId, message); + }); } WorkerInspectorClient::~WorkerInspectorClient() { @@ -74,22 +77,27 @@ StringView Make8BitStringView(const std::string& value) { v8::Locker locker(isolate_); Isolate::Scope isolate_scope(isolate_); HandleScope handle_scope(isolate_); - if (session_ != nullptr) { - session_->resume(); - session_.reset(); - } + sessions_.reset(); inspector_.reset(); context_.Reset(); } -void WorkerInspectorClient::PushMessage(const std::string& message) { +void WorkerInspectorClient::PushMessage(uint64_t connectionId, const std::string& message) { + this->PushIncoming({connectionId, false, message}); +} + +void WorkerInspectorClient::PushDisconnect(uint64_t connectionId) { + this->PushIncoming({connectionId, true, std::string()}); +} + +void WorkerInspectorClient::PushIncoming(Incoming incoming) { if (dying_) { return; } { std::lock_guard lock(incomingMutex_); - incoming_.push(message); + incoming_.push(std::move(incoming)); } if (source_ != nullptr && CFRunLoopSourceIsValid(source_)) { @@ -99,101 +107,51 @@ StringView Make8BitStringView(const std::string& value) { dispatch_semaphore_signal(messageArrived_); } -std::string WorkerInspectorClient::PopMessage() { +bool WorkerInspectorClient::PopIncoming(Incoming& incoming) { std::lock_guard lock(incomingMutex_); if (incoming_.empty()) { - return ""; + return false; } - std::string message = incoming_.front(); + incoming = std::move(incoming_.front()); incoming_.pop(); - return message; + return true; } void WorkerInspectorClient::DrainIncoming() { - std::string message; - while (!dying_ && !(message = this->PopMessage()).empty()) { - this->DispatchOne(message); + Incoming incoming; + while (!dying_ && this->PopIncoming(incoming)) { + this->HandleIncoming(incoming); } - this->MaybeResetSession(); } -void WorkerInspectorClient::DispatchOne(const std::string& message) { - if (message == kResetSessionMessage) { - this->HandleResetRequest(); +void WorkerInspectorClient::HandleIncoming(const Incoming& incoming) { + if (incoming.disconnect) { + sessions_->Disconnect(incoming.connectionId); return; } - if (session_ == nullptr) { - return; - } + sessions_->Connect(inspector_.get(), incoming.connectionId); + sessions_->Dispatch(incoming.connectionId, incoming.message); v8::Locker locker(isolate_); Isolate::Scope isolate_scope(isolate_); HandleScope handle_scope(isolate_); - session_->dispatchProtocolMessage(Make8BitStringView(message)); isolate_->PerformMicrotaskCheckpoint(); } -void WorkerInspectorClient::HandleResetRequest() { - if (runningPauseLoop_) { - // We're inside session_->dispatchProtocolMessage somewhere up the stack — - // resume now (which exits the nested pause loop) and swap the session - // only once that stack has fully unwound, from DrainIncoming. - pendingReset_ = true; - { - v8::Locker locker(isolate_); - Isolate::Scope isolate_scope(isolate_); - HandleScope handle_scope(isolate_); - if (session_ != nullptr) { - session_->resume(); - } - } - if (source_ != nullptr && CFRunLoopSourceIsValid(source_)) { - CFRunLoopSourceSignal(source_); - CFRunLoopWakeUp(workerLoop_); - } - return; - } - - this->DoResetSession(); -} - -void WorkerInspectorClient::DoResetSession() { - v8::Locker locker(isolate_); - Isolate::Scope isolate_scope(isolate_); - HandleScope handle_scope(isolate_); - if (session_ != nullptr) { - session_->resume(); - session_.reset(); - } - session_ = - inspector_->connect(contextGroupId, this, {}, v8_inspector::V8Inspector::kFullyTrusted); - - JsV8InspectorClient* root = JsV8InspectorClient::GetInstance(); - if (root != nullptr) { - frontendGeneration_ = root->FrontendGeneration(); - } -} - -void WorkerInspectorClient::MaybeResetSession() { - if (pendingReset_ && !runningPauseLoop_ && !dying_) { - pendingReset_ = false; - this->DoResetSession(); - } -} - void WorkerInspectorClient::runMessageLoopOnPause(int contextGroupId) { if (runningPauseLoop_ || dying_) { return; } runningPauseLoop_ = true; pauseTerminated_ = false; + sessions_->ClearPauseRequests(); while (!pauseTerminated_ && !dying_) { - std::string message = this->PopMessage(); - bool shouldWait = message.empty(); + Incoming incoming; + bool shouldWait = !this->PopIncoming(incoming); if (!shouldWait) { - this->DispatchOne(message); + this->HandleIncoming(incoming); } // JS frames are on the stack, so only nestable v8 foreground tasks may @@ -221,7 +179,8 @@ StringView Make8BitStringView(const std::string& value) { dispatch_semaphore_signal(messageArrived_); } -void WorkerInspectorClient::RequestPauseInterrupt() { +void WorkerInspectorClient::RequestPauseInterrupt(uint64_t connectionId) { + sessions_->RequestPause(connectionId); isolate_->RequestInterrupt( [](Isolate* isolate, void* data) { // Runs on the worker thread mid-JS. Teardown also happens on the @@ -237,23 +196,9 @@ StringView Make8BitStringView(const std::string& value) { reinterpret_cast(static_cast(workerId_))); } -void WorkerInspectorClient::SchedulePauseFromInterrupt() { - if (session_ != nullptr) { - session_->schedulePauseOnNextStatement({}, {}); - } -} - -void WorkerInspectorClient::sendResponse(int callId, std::unique_ptr message) { - this->SendWrapped(ToStdString(message->string())); -} - -void WorkerInspectorClient::sendNotification(std::unique_ptr message) { - this->SendWrapped(ToStdString(message->string())); -} - -void WorkerInspectorClient::flushProtocolNotifications() {} +void WorkerInspectorClient::SchedulePauseFromInterrupt() { sessions_->TakePauseRequests(); } -void WorkerInspectorClient::SendWrapped(const std::string& message) { +void WorkerInspectorClient::SendWrapped(uint64_t connectionId, const std::string& message) { if (message.empty() || message[0] != '{') { return; } @@ -272,7 +217,7 @@ StringView Make8BitStringView(const std::string& value) { JsV8InspectorClient* root = JsV8InspectorClient::GetInstance(); if (root != nullptr) { - root->SendWorkerOutput(frontendGeneration_, wrapped); + root->SendToConnection(connectionId, wrapped); } } @@ -296,7 +241,7 @@ StringView Make8BitStringView(const std::string& value) { context, contextId, contextGroupId, impl, currentTimeMS(), method, {args.data(), args.size()}, String16{}, std::move(stackImpl)); - // Going through the message storage both reports to the session when the + // Going through the message storage both reports to the sessions whose // frontend has enabled the Runtime agent AND keeps the message for replay // on Runtime.enable. Workers log most of their output (module top-level, // early onmessage work) before DevTools attaches and enables the session; diff --git a/v8ios.xcodeproj/project.pbxproj b/v8ios.xcodeproj/project.pbxproj index 40102693..afb4334c 100644 --- a/v8ios.xcodeproj/project.pbxproj +++ b/v8ios.xcodeproj/project.pbxproj @@ -338,6 +338,8 @@ F6191AB029C0FCE8003F588F /* JsV8InspectorClient.mm in Sources */ = {isa = PBXBuildFile; fileRef = F6191AA829C0FCE7003F588F /* JsV8InspectorClient.mm */; }; A1B2C3D40000000000000003 /* WorkerInspectorClient.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000001 /* WorkerInspectorClient.h */; }; A1B2C3D40000000000000004 /* WorkerInspectorClient.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000002 /* WorkerInspectorClient.mm */; }; + A1B2C3D40000000000000007 /* InspectorSessions.h in Headers */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000005 /* InspectorSessions.h */; }; + A1B2C3D40000000000000008 /* InspectorSessions.mm in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D40000000000000006 /* InspectorSessions.mm */; }; F6191AB129C0FCE8003F588F /* InspectorServer.h in Headers */ = {isa = PBXBuildFile; fileRef = F6191AA929C0FCE7003F588F /* InspectorServer.h */; }; F6191AB229C0FCE8003F588F /* InspectorServer.mm in Sources */ = {isa = PBXBuildFile; fileRef = F6191AAA29C0FCE7003F588F /* InspectorServer.mm */; settings = {COMPILER_FLAGS = "-fobjc-arc"; }; }; F6191AB629C0FF87003F588F /* utils.h in Headers */ = {isa = PBXBuildFile; fileRef = F6191AB429C0FF86003F588F /* utils.h */; }; @@ -872,6 +874,8 @@ F6191AA829C0FCE7003F588F /* JsV8InspectorClient.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = JsV8InspectorClient.mm; sourceTree = ""; }; A1B2C3D40000000000000001 /* WorkerInspectorClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WorkerInspectorClient.h; sourceTree = ""; }; A1B2C3D40000000000000002 /* WorkerInspectorClient.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = WorkerInspectorClient.mm; sourceTree = ""; }; + A1B2C3D40000000000000005 /* InspectorSessions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorSessions.h; sourceTree = ""; }; + A1B2C3D40000000000000006 /* InspectorSessions.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = InspectorSessions.mm; sourceTree = ""; }; F6191AA929C0FCE7003F588F /* InspectorServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InspectorServer.h; sourceTree = ""; }; F6191AAA29C0FCE7003F588F /* InspectorServer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = InspectorServer.mm; sourceTree = ""; }; F6191AB429C0FF86003F588F /* utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = utils.h; sourceTree = ""; }; @@ -1636,6 +1640,8 @@ F6191AA829C0FCE7003F588F /* JsV8InspectorClient.mm */, A1B2C3D40000000000000001 /* WorkerInspectorClient.h */, A1B2C3D40000000000000002 /* WorkerInspectorClient.mm */, + A1B2C3D40000000000000005 /* InspectorSessions.h */, + A1B2C3D40000000000000006 /* InspectorSessions.mm */, F6191AA929C0FCE7003F588F /* InspectorServer.h */, F6191AAA29C0FCE7003F588F /* InspectorServer.mm */, 91B25A0829DAC83D00E3CE04 /* ns-v8-tracing-agent-impl.mm */, @@ -1678,6 +1684,7 @@ F1F30E772B58FC74006A62C0 /* URLImpl.h in Headers */, F6191AAE29C0FCE8003F588F /* JsV8InspectorClient.h in Headers */, A1B2C3D40000000000000003 /* WorkerInspectorClient.h in Headers */, + A1B2C3D40000000000000007 /* InspectorSessions.h in Headers */, 6573B9D0291FE29F00B0ED7C /* JSIV8ValueConverter.h in Headers */, C20AB5E726E1015300E2B41D /* OneByteStringResource.h in Headers */, C2DDEBAC229EAC8300345BFE /* ObjectManager.h in Headers */, @@ -2324,6 +2331,7 @@ C2DDEBAF229EAC8300345BFE /* ObjectManager.mm in Sources */, F6191AB029C0FCE8003F588F /* JsV8InspectorClient.mm in Sources */, A1B2C3D40000000000000004 /* WorkerInspectorClient.mm in Sources */, + A1B2C3D40000000000000008 /* InspectorSessions.mm in Sources */, C2DDEB9C229EAC8300345BFE /* ClassBuilder.cpp in Sources */, C266569322AFFF7E00EE15CC /* Pointer.cpp in Sources */, F1CB51832D5C37100042555E /* URLPatternImpl.cpp in Sources */,