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 */,