diff --git a/include/js_protocol.pdl b/include/js_protocol.pdl index 3231e11c9d..eb907490ab 100644 --- a/include/js_protocol.pdl +++ b/include/js_protocol.pdl @@ -1226,6 +1226,10 @@ domain Runtime string origin # Human readable name describing given context. string name + # A system-unique execution context identifier. Unlike the id, this is unique accross + # multiple processes, so can be reliably used to identify specific context while backend + # performs a cross-process navigation. + experimental string uniqueId # Embedder-specific auxiliary data. optional object auxData @@ -1389,6 +1393,9 @@ domain Runtime optional boolean silent # Specifies in which execution context to perform evaluation. If the parameter is omitted the # evaluation will be performed in the context of the inspected page. + # This is mutually exclusive with `uniqueContextId`, which offers an + # alternative way to identify the execution context that is more reliable + # in a multi-process environment. optional ExecutionContextId contextId # Whether the result is expected to be a JSON object that should be sent by value. optional boolean returnByValue @@ -1415,6 +1422,13 @@ domain Runtime # when called with non-callable arguments. This flag bypasses CSP for this # evaluation and allows unsafe-eval. Defaults to true. experimental optional boolean allowUnsafeEvalBlockedByCSP + # An alternative way to specify the execution context to evaluate in. + # Compared to contextId that may be reused accross processes, this is guaranteed to be + # system-unique, so it can be used to prevent accidental evaluation of the expression + # in context different than intended (e.g. as a result of navigation accross process + # boundaries). + # This is mutually exclusive with `contextId`. + experimental optional string uniqueContextId returns # Evaluation result. RemoteObject result diff --git a/src/inspector/BUILD.gn b/src/inspector/BUILD.gn index acb1e961a8..495354da45 100644 --- a/src/inspector/BUILD.gn +++ b/src/inspector/BUILD.gn @@ -124,6 +124,8 @@ v8_source_set("inspector") { "v8-console.h", "v8-debugger-agent-impl.cc", "v8-debugger-agent-impl.h", + "v8-debugger-id.cc", + "v8-debugger-id.h", "v8-debugger-script.cc", "v8-debugger-script.h", "v8-debugger.cc", diff --git a/src/inspector/inspected-context.cc b/src/inspector/inspected-context.cc index 52a161cf29..a47df1ef12 100644 --- a/src/inspector/inspected-context.cc +++ b/src/inspector/inspected-context.cc @@ -55,7 +55,8 @@ InspectedContext::InspectedContext(V8InspectorImpl* inspector, m_contextGroupId(info.contextGroupId), m_origin(toString16(info.origin)), m_humanReadableName(toString16(info.humanReadableName)), - m_auxData(toString16(info.auxData)) { + m_auxData(toString16(info.auxData)), + m_uniqueId(V8DebuggerId::generate(inspector)) { v8::debug::SetContextId(info.context, contextId); m_weakCallbackData = new WeakCallbackData(this, m_inspector, m_contextGroupId, m_contextId); diff --git a/src/inspector/inspected-context.h b/src/inspector/inspected-context.h index aeea50b12c..d3f0fe012b 100644 --- a/src/inspector/inspected-context.h +++ b/src/inspector/inspected-context.h @@ -9,11 +9,11 @@ #include #include +#include "include/v8.h" #include "src/base/macros.h" #include "src/debug/debug-interface.h" #include "src/inspector/string-16.h" - -#include "include/v8.h" +#include "src/inspector/v8-debugger-id.h" namespace v8_inspector { @@ -37,6 +37,7 @@ class InspectedContext { int contextGroupId() const { return m_contextGroupId; } String16 origin() const { return m_origin; } String16 humanReadableName() const { return m_humanReadableName; } + V8DebuggerId uniqueId() const { return m_uniqueId; } String16 auxData() const { return m_auxData; } bool isReported(int sessionId) const; @@ -66,6 +67,7 @@ class InspectedContext { const String16 m_origin; const String16 m_humanReadableName; const String16 m_auxData; + const V8DebuggerId m_uniqueId; std::unordered_set m_reportedSessionIds; std::unordered_map> m_injectedScripts; WeakCallbackData* m_weakCallbackData; diff --git a/src/inspector/v8-debugger-id.cc b/src/inspector/v8-debugger-id.cc new file mode 100644 index 0000000000..995df6689f --- /dev/null +++ b/src/inspector/v8-debugger-id.cc @@ -0,0 +1,45 @@ +// Copyright 2020 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "src/inspector/v8-debugger-id.h" + +#include "src/debug/debug-interface.h" +#include "src/inspector/v8-inspector-impl.h" + +namespace v8_inspector { + +V8DebuggerId::V8DebuggerId(std::pair pair) + : m_first(pair.first), m_second(pair.second) {} + +// static +V8DebuggerId V8DebuggerId::generate(V8InspectorImpl* inspector) { + return V8DebuggerId(std::make_pair(inspector->generateUniqueId(), + inspector->generateUniqueId())); +} + +V8DebuggerId::V8DebuggerId(const String16& debuggerId) { + const UChar dot = '.'; + size_t pos = debuggerId.find(dot); + if (pos == String16::kNotFound) return; + bool ok = false; + int64_t first = debuggerId.substring(0, pos).toInteger64(&ok); + if (!ok) return; + int64_t second = debuggerId.substring(pos + 1).toInteger64(&ok); + if (!ok) return; + m_first = first; + m_second = second; +} + +String16 V8DebuggerId::toString() const { + return String16::fromInteger64(m_first) + "." + + String16::fromInteger64(m_second); +} + +bool V8DebuggerId::isValid() const { return m_first || m_second; } + +std::pair V8DebuggerId::pair() const { + return std::make_pair(m_first, m_second); +} + +} // namespace v8_inspector diff --git a/src/inspector/v8-debugger-id.h b/src/inspector/v8-debugger-id.h new file mode 100644 index 0000000000..5f53c02189 --- /dev/null +++ b/src/inspector/v8-debugger-id.h @@ -0,0 +1,44 @@ +// Copyright 2020 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef V8_INSPECTOR_V8_DEBUGGER_ID_H_ +#define V8_INSPECTOR_V8_DEBUGGER_ID_H_ + +#include + +#include "src/base/macros.h" +#include "src/inspector/protocol/Forward.h" + +namespace v8_inspector { + +class V8InspectorImpl; + +// This debugger id tries to be unique by generating two random +// numbers, which should most likely avoid collisions. +// Debugger id has a 1:1 mapping to context group. It is used to +// attribute stack traces to a particular debugging, when doing any +// cross-debugger operations (e.g. async step in). +// See also Runtime.UniqueDebuggerId in the protocol. +class V8DebuggerId { + public: + V8DebuggerId() = default; + explicit V8DebuggerId(std::pair); + explicit V8DebuggerId(const String16&); + V8DebuggerId(const V8DebuggerId&) V8_NOEXCEPT = default; + ~V8DebuggerId() = default; + + static V8DebuggerId generate(V8InspectorImpl*); + + String16 toString() const; + bool isValid() const; + std::pair pair() const; + + private: + int64_t m_first = 0; + int64_t m_second = 0; +}; + +} // namespace v8_inspector + +#endif // V8_INSPECTOR_V8_DEBUGGER_ID_H_ diff --git a/src/inspector/v8-debugger.cc b/src/inspector/v8-debugger.cc index 302b218f20..c2ecf22e32 100644 --- a/src/inspector/v8-debugger.cc +++ b/src/inspector/v8-debugger.cc @@ -64,39 +64,6 @@ class MatchPrototypePredicate : public v8::debug::QueryObjectPredicate { } // namespace -V8DebuggerId::V8DebuggerId(std::pair pair) - : m_first(pair.first), m_second(pair.second) {} - -// static -V8DebuggerId V8DebuggerId::generate(V8InspectorImpl* inspector) { - return V8DebuggerId(std::make_pair(inspector->generateUniqueId(), - inspector->generateUniqueId())); -} - -V8DebuggerId::V8DebuggerId(const String16& debuggerId) { - const UChar dot = '.'; - size_t pos = debuggerId.find(dot); - if (pos == String16::kNotFound) return; - bool ok = false; - int64_t first = debuggerId.substring(0, pos).toInteger64(&ok); - if (!ok) return; - int64_t second = debuggerId.substring(pos + 1).toInteger64(&ok); - if (!ok) return; - m_first = first; - m_second = second; -} - -String16 V8DebuggerId::toString() const { - return String16::fromInteger64(m_first) + "." + - String16::fromInteger64(m_second); -} - -bool V8DebuggerId::isValid() const { return m_first || m_second; } - -std::pair V8DebuggerId::pair() const { - return std::make_pair(m_first, m_second); -} - V8Debugger::V8Debugger(v8::Isolate* isolate, V8InspectorImpl* inspector) : m_isolate(isolate), m_inspector(inspector), diff --git a/src/inspector/v8-debugger.h b/src/inspector/v8-debugger.h index 1368d4a421..fc790a9327 100644 --- a/src/inspector/v8-debugger.h +++ b/src/inspector/v8-debugger.h @@ -11,15 +11,15 @@ #include #include +#include "include/v8-inspector.h" #include "src/base/macros.h" #include "src/inspector/inspected-context.h" #include "src/inspector/protocol/Debugger.h" #include "src/inspector/protocol/Forward.h" #include "src/inspector/protocol/Runtime.h" +#include "src/inspector/v8-debugger-id.h" #include "src/inspector/v8-debugger-script.h" -#include "include/v8-inspector.h" - namespace v8_inspector { class AsyncStackTrace; @@ -36,31 +36,6 @@ using protocol::Response; using TerminateExecutionCallback = protocol::Runtime::Backend::TerminateExecutionCallback; -// This debugger id tries to be unique by generating two random -// numbers, which should most likely avoid collisions. -// Debugger id has a 1:1 mapping to context group. It is used to -// attribute stack traces to a particular debugging, when doing any -// cross-debugger operations (e.g. async step in). -// See also Runtime.UniqueDebuggerId in the protocol. -class V8DebuggerId { - public: - V8DebuggerId() = default; - explicit V8DebuggerId(std::pair); - explicit V8DebuggerId(const String16&); - V8DebuggerId(const V8DebuggerId&) V8_NOEXCEPT = default; - ~V8DebuggerId() = default; - - static V8DebuggerId generate(V8InspectorImpl*); - - String16 toString() const; - bool isValid() const; - std::pair pair() const; - - private: - int64_t m_first = 0; - int64_t m_second = 0; -}; - class V8Debugger : public v8::debug::DebugDelegate, public v8::debug::AsyncEventDelegate { public: diff --git a/src/inspector/v8-inspector-impl.cc b/src/inspector/v8-inspector-impl.cc index 871dcb5f26..3d51aa7f6a 100644 --- a/src/inspector/v8-inspector-impl.cc +++ b/src/inspector/v8-inspector-impl.cc @@ -81,6 +81,11 @@ int V8InspectorImpl::contextGroupId(int contextId) const { return it != m_contextIdToGroupIdMap.end() ? it->second : 0; } +int V8InspectorImpl::resolveUniqueContextId(V8DebuggerId uniqueId) const { + auto it = m_uniqueIdToContextId.find(uniqueId.pair()); + return it == m_uniqueIdToContextId.end() ? 0 : it->second; +} + v8::MaybeLocal V8InspectorImpl::compileAndRunInternalScript( v8::Local context, v8::Local source) { v8::Local unboundScript; @@ -190,6 +195,11 @@ void V8InspectorImpl::contextCreated(const V8ContextInfo& info) { auto* context = new InspectedContext(this, info, contextId); m_contextIdToGroupIdMap[contextId] = info.contextGroupId; + DCHECK(m_uniqueIdToContextId.find(context->uniqueId().pair()) == + m_uniqueIdToContextId.end()); + m_uniqueIdToContextId.insert( + std::make_pair(context->uniqueId().pair(), contextId)); + auto contextIt = m_contexts.find(info.contextGroupId); if (contextIt == m_contexts.end()) contextIt = m_contexts @@ -233,14 +243,15 @@ void V8InspectorImpl::contextCollected(int groupId, int contextId) { void V8InspectorImpl::resetContextGroup(int contextGroupId) { m_consoleStorageMap.erase(contextGroupId); m_muteExceptionsMap.erase(contextGroupId); - std::vector contextIdsToClear; - forEachContext(contextGroupId, - [&contextIdsToClear](InspectedContext* context) { - contextIdsToClear.push_back(context->contextId()); - }); + auto contextsIt = m_contexts.find(contextGroupId); + // Context might have been removed already by discardContextScript() + if (contextsIt != m_contexts.end()) { + for (const auto& map_entry : *contextsIt->second) + m_uniqueIdToContextId.erase(map_entry.second->uniqueId().pair()); + m_contexts.erase(contextsIt); + } forEachSession(contextGroupId, [](V8InspectorSessionImpl* session) { session->reset(); }); - m_contexts.erase(contextGroupId); } void V8InspectorImpl::idleStarted() { m_isolate->SetIdle(true); } @@ -362,7 +373,9 @@ v8::Local V8InspectorImpl::regexContext() { void V8InspectorImpl::discardInspectedContext(int contextGroupId, int contextId) { - if (!getContext(contextGroupId, contextId)) return; + auto* context = getContext(contextGroupId, contextId); + if (!context) return; + m_uniqueIdToContextId.erase(context->uniqueId().pair()); m_contexts[contextGroupId]->erase(contextId); if (m_contexts[contextGroupId]->empty()) m_contexts.erase(contextGroupId); } diff --git a/src/inspector/v8-inspector-impl.h b/src/inspector/v8-inspector-impl.h index dfb5353505..c5259b0c60 100644 --- a/src/inspector/v8-inspector-impl.h +++ b/src/inspector/v8-inspector-impl.h @@ -68,6 +68,7 @@ class V8InspectorImpl : public V8Inspector { int contextGroupId(v8::Local) const; int contextGroupId(int contextId) const; uint64_t isolateId() const { return m_isolateId; } + int resolveUniqueContextId(V8DebuggerId uniqueId) const; v8::MaybeLocal compileAndRunInternalScript(v8::Local, v8::Local); @@ -178,6 +179,7 @@ class V8InspectorImpl : public V8Inspector { ConsoleStorageMap m_consoleStorageMap; std::unordered_map m_contextIdToGroupIdMap; + std::map, int> m_uniqueIdToContextId; std::unique_ptr m_console; diff --git a/src/inspector/v8-runtime-agent-impl.cc b/src/inspector/v8-runtime-agent-impl.cc index 29afa616e6..6fee3b3b05 100644 --- a/src/inspector/v8-runtime-agent-impl.cc +++ b/src/inspector/v8-runtime-agent-impl.cc @@ -204,9 +204,21 @@ void innerCallFunctionOn( } Response ensureContext(V8InspectorImpl* inspector, int contextGroupId, - Maybe executionContextId, int* contextId) { + Maybe executionContextId, + Maybe uniqueContextId, int* contextId) { if (executionContextId.isJust()) { + if (uniqueContextId.isJust()) { + return Response::InvalidParams( + "contextId and uniqueContextId are mutually exclusive"); + } *contextId = executionContextId.fromJust(); + } else if (uniqueContextId.isJust()) { + V8DebuggerId uniqueId(uniqueContextId.fromJust()); + if (!uniqueId.isValid()) + return Response::InvalidParams("invalid uniqueContextId"); + int id = inspector->resolveUniqueContextId(uniqueId); + if (!id) return Response::InvalidParams("uniqueContextId not found"); + *contextId = id; } else { v8::HandleScope handles(inspector->isolate()); v8::Local defaultContext = @@ -215,6 +227,7 @@ Response ensureContext(V8InspectorImpl* inspector, int contextGroupId, return Response::ServerError("Cannot find default execution context"); *contextId = InspectedContext::contextId(defaultContext); } + return Response::Success(); } @@ -238,13 +251,14 @@ void V8RuntimeAgentImpl::evaluate( Maybe generatePreview, Maybe userGesture, Maybe maybeAwaitPromise, Maybe throwOnSideEffect, Maybe timeout, Maybe disableBreaks, Maybe maybeReplMode, - Maybe allowUnsafeEvalBlockedByCSP, + Maybe allowUnsafeEvalBlockedByCSP, Maybe uniqueContextId, std::unique_ptr callback) { TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), "EvaluateScript"); int contextId = 0; Response response = ensureContext(m_inspector, m_session->contextGroupId(), - std::move(executionContextId), &contextId); + std::move(executionContextId), + std::move(uniqueContextId), &contextId); if (!response.IsSuccess()) { callback->sendFailure(response); return; @@ -378,9 +392,9 @@ void V8RuntimeAgentImpl::callFunctionOn( std::move(callback)); } else { int contextId = 0; - Response response = - ensureContext(m_inspector, m_session->contextGroupId(), - std::move(executionContextId.fromJust()), &contextId); + Response response = ensureContext(m_inspector, m_session->contextGroupId(), + std::move(executionContextId.fromJust()), + /* uniqueContextId */ {}, &contextId); if (!response.IsSuccess()) { callback->sendFailure(response); return; @@ -497,7 +511,8 @@ Response V8RuntimeAgentImpl::compileScript( int contextId = 0; Response response = ensureContext(m_inspector, m_session->contextGroupId(), - std::move(executionContextId), &contextId); + std::move(executionContextId), + /*uniqueContextId*/ {}, &contextId); if (!response.IsSuccess()) return response; InjectedScript::ContextScope scope(m_session, contextId); response = scope.initialize(); @@ -550,7 +565,8 @@ void V8RuntimeAgentImpl::runScript( int contextId = 0; Response response = ensureContext(m_inspector, m_session->contextGroupId(), - std::move(executionContextId), &contextId); + std::move(executionContextId), + /*uniqueContextId*/ {}, &contextId); if (!response.IsSuccess()) { callback->sendFailure(response); return; @@ -626,7 +642,8 @@ Response V8RuntimeAgentImpl::globalLexicalScopeNames( std::unique_ptr>* outNames) { int contextId = 0; Response response = ensureContext(m_inspector, m_session->contextGroupId(), - std::move(executionContextId), &contextId); + std::move(executionContextId), + /*uniqueContextId*/ {}, &contextId); if (!response.IsSuccess()) return response; InjectedScript::ContextScope scope(m_session, contextId); @@ -864,6 +881,7 @@ void V8RuntimeAgentImpl::reportExecutionContextCreated( .setId(context->contextId()) .setName(context->humanReadableName()) .setOrigin(context->origin()) + .setUniqueId(context->uniqueId().toString()) .build(); const String16& aux = context->auxData(); if (!aux.isEmpty()) { diff --git a/src/inspector/v8-runtime-agent-impl.h b/src/inspector/v8-runtime-agent-impl.h index 3b96dcef2e..5ac1e462f8 100644 --- a/src/inspector/v8-runtime-agent-impl.h +++ b/src/inspector/v8-runtime-agent-impl.h @@ -71,6 +71,7 @@ class V8RuntimeAgentImpl : public protocol::Runtime::Backend { Maybe awaitPromise, Maybe throwOnSideEffect, Maybe timeout, Maybe disableBreaks, Maybe replMode, Maybe allowUnsafeEvalBlockedByCSP, + Maybe uniqueContextId, std::unique_ptr) override; void awaitPromise(const String16& promiseObjectId, Maybe returnByValue, Maybe generatePreview, diff --git a/test/inspector/protocol-test.js b/test/inspector/protocol-test.js index 89b663f052..2b0f571e16 100644 --- a/test/inspector/protocol-test.js +++ b/test/inspector/protocol-test.js @@ -36,7 +36,7 @@ InspectorTest.logMessage = function(originalMessage) { const nonStableFields = new Set([ 'objectId', 'scriptId', 'exceptionId', 'timestamp', 'executionContextId', 'callFrameId', 'breakpointId', 'bindRemoteObjectFunctionId', - 'formatterObjectId', 'debuggerId', 'bodyGetterId' + 'formatterObjectId', 'debuggerId', 'bodyGetterId', 'uniqueId' ]); const message = JSON.parse(JSON.stringify(originalMessage, replacer.bind(null, Symbol(), nonStableFields))); if (message.id) diff --git a/test/inspector/runtime/create-context-expected.txt b/test/inspector/runtime/create-context-expected.txt index 770d2e32d2..2141b0bab9 100644 --- a/test/inspector/runtime/create-context-expected.txt +++ b/test/inspector/runtime/create-context-expected.txt @@ -6,6 +6,7 @@ Checks createContext(). id : 1 name : origin : + uniqueId : } } } @@ -16,6 +17,7 @@ Checks createContext(). id : 2 name : origin : + uniqueId : } } } @@ -37,6 +39,7 @@ Reported script's execution id: 2 id : 1 name : origin : + uniqueId : } } } @@ -47,6 +50,7 @@ Reported script's execution id: 2 id : 2 name : origin : + uniqueId : } } } diff --git a/test/inspector/runtime/evaluate-unique-context-id-expected.txt b/test/inspector/runtime/evaluate-unique-context-id-expected.txt new file mode 100644 index 0000000000..708ac401f6 --- /dev/null +++ b/test/inspector/runtime/evaluate-unique-context-id-expected.txt @@ -0,0 +1,24 @@ +Tests how Runtime.evaluate handles uniqueContextId argument +token in context 1: context 1 +token in context 2: context 2 +{ + error : { + code : -32602 + message : contextId and uniqueContextId are mutually exclusive + } + id : +} +{ + error : { + code : -32602 + message : invalid uniqueContextId + } + id : +} +{ + error : { + code : -32602 + message : uniqueContextId not found + } + id : +} diff --git a/test/inspector/runtime/evaluate-unique-context-id.js b/test/inspector/runtime/evaluate-unique-context-id.js new file mode 100644 index 0000000000..21eddfb26e --- /dev/null +++ b/test/inspector/runtime/evaluate-unique-context-id.js @@ -0,0 +1,66 @@ +// Copyright 2020 the V8 project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +const {Protocol, contextGroup} = InspectorTest.start( + `Tests how Runtime.evaluate handles uniqueContextId argument`); + +(async function test(){ + Protocol.Runtime.enable(); + const context1 = (await Protocol.Runtime.onceExecutionContextCreated()).params.context; + + contextGroup.createContext(); + const context2 = (await Protocol.Runtime.onceExecutionContextCreated()).params.context; + + Protocol.Runtime.evaluate({ + expression: 'token = "context 1";', + contextId: context1.id + }); + Protocol.Runtime.evaluate({ + expression: 'token = "context 2";', + contextId: context2.id + }); + + { + const response = (await Protocol.Runtime.evaluate({ + expression: 'token', + uniqueContextId: context1.uniqueId, + returnByValue: true + })).result.result.value; + InspectorTest.logMessage(`token in context 1: ${response}`); + } + { + const response = (await Protocol.Runtime.evaluate({ + expression: 'token', + uniqueContextId: context2.uniqueId, + returnByValue: true + })).result.result.value; + InspectorTest.logMessage(`token in context 2: ${response}`); + } + + // The following tests are for error handling. + { + const response = (await Protocol.Runtime.evaluate({ + expression: 'token', + uniqueContextId: context1.uniqueId, + contextId: context1.id + })); + InspectorTest.logMessage(response); + } + { + const response = (await Protocol.Runtime.evaluate({ + expression: 'token', + uniqueContextId: 'fubar', + })); + InspectorTest.logMessage(response); + } + { + const response = (await Protocol.Runtime.evaluate({ + expression: 'token', + uniqueContextId: context1.uniqueId + 1, + })); + InspectorTest.logMessage(response); + } + + InspectorTest.completeTest(); +})(); diff --git a/test/inspector/runtime/runtime-restore-expected.txt b/test/inspector/runtime/runtime-restore-expected.txt index 1810872f0b..0072588abc 100644 --- a/test/inspector/runtime/runtime-restore-expected.txt +++ b/test/inspector/runtime/runtime-restore-expected.txt @@ -8,6 +8,7 @@ Running test: testExecutionContextsNotificationsOnRestore id : 1 name : origin : + uniqueId : } } } @@ -24,6 +25,7 @@ will reconnect.. id : 1 name : origin : + uniqueId : } } } diff --git a/test/inspector/sessions/create-session-expected.txt b/test/inspector/sessions/create-session-expected.txt index e3d921e3b7..4459f4d19c 100644 --- a/test/inspector/sessions/create-session-expected.txt +++ b/test/inspector/sessions/create-session-expected.txt @@ -8,6 +8,7 @@ From session 1 id : 1 name : origin : + uniqueId : } } } @@ -20,6 +21,7 @@ From session 2 id : 1 name : origin : + uniqueId : } } } @@ -32,6 +34,7 @@ From session 2 id : 1 name : origin : + uniqueId : } } } @@ -44,6 +47,7 @@ From session 1 id : 1 name : origin : + uniqueId : } } } @@ -56,6 +60,7 @@ From session 3 id : 1 name : origin : + uniqueId : } } } @@ -92,6 +97,7 @@ From session 2 id : 2 name : origin : + uniqueId : } } } @@ -103,6 +109,7 @@ From session 1 id : 2 name : origin : + uniqueId : } } } @@ -114,6 +121,7 @@ From session 3 id : 2 name : origin : + uniqueId : } } } @@ -127,6 +135,7 @@ From session 4 id : 2 name : origin : + uniqueId : } } }