Commit 375bea1c authored by dgozman's avatar dgozman Committed by Commit Bot

[inspector] Support multiple sessions per context group

This patch adds ability to connect multiple sessions to a single context group. This is an experimental feature, which is already supported in test harness.

So far covered runtime domain with tests (and found a bug thanks to the test). More tests to follow in next patches, probably with code adjustments as well.

BUG=chromium:590878

Review-Url: https://codereview.chromium.org/2906153002
Cr-Commit-Position: refs/heads/master@{#45667}
parent fa89ce53
......@@ -428,7 +428,8 @@ InjectedScript::Scope::Scope(V8InspectorSessionImpl* session)
Response InjectedScript::Scope::initialize() {
cleanup();
V8InspectorSessionImpl* session = m_inspector->sessionById(m_sessionId);
V8InspectorSessionImpl* session =
m_inspector->sessionById(m_contextGroupId, m_sessionId);
if (!session) return Response::InternalError();
Response response = findInjectedScript(session);
if (!response.isSuccess()) return response;
......
......@@ -23,8 +23,7 @@ InspectedContext::InspectedContext(V8InspectorImpl* inspector,
m_contextGroupId(info.contextGroupId),
m_origin(toString16(info.origin)),
m_humanReadableName(toString16(info.humanReadableName)),
m_auxData(toString16(info.auxData)),
m_reported(false) {
m_auxData(toString16(info.auxData)) {
v8::debug::SetContextId(info.context, contextId);
if (!info.hasMemoryOnConsole) return;
v8::Context::Scope contextScope(info.context);
......@@ -54,6 +53,17 @@ v8::Isolate* InspectedContext::isolate() const {
return m_inspector->isolate();
}
bool InspectedContext::isReported(int sessionId) const {
return m_reportedSessionIds.find(sessionId) != m_reportedSessionIds.cend();
}
void InspectedContext::setReported(int sessionId, bool reported) {
if (reported)
m_reportedSessionIds.insert(sessionId);
else
m_reportedSessionIds.erase(sessionId);
}
bool InspectedContext::createInjectedScript() {
DCHECK(!m_injectedScript);
std::unique_ptr<InjectedScript> injectedScript = InjectedScript::create(this);
......
......@@ -5,6 +5,9 @@
#ifndef V8_INSPECTOR_INSPECTEDCONTEXT_H_
#define V8_INSPECTOR_INSPECTEDCONTEXT_H_
#include <map>
#include <unordered_set>
#include "src/base/macros.h"
#include "src/inspector/string-16.h"
......@@ -30,8 +33,8 @@ class InspectedContext {
String16 humanReadableName() const { return m_humanReadableName; }
String16 auxData() const { return m_auxData; }
bool isReported() const { return m_reported; }
void setReported(bool reported) { m_reported = reported; }
bool isReported(int sessionId) const;
void setReported(int sessionId, bool reported);
v8::Isolate* isolate() const;
V8InspectorImpl* inspector() const { return m_inspector; }
......@@ -51,7 +54,7 @@ class InspectedContext {
const String16 m_origin;
const String16 m_humanReadableName;
const String16 m_auxData;
bool m_reported;
std::unordered_set<int> m_reportedSessionIds;
std::unique_ptr<InjectedScript> m_injectedScript;
DISALLOW_COPY_AND_ASSIGN(InspectedContext);
......
......@@ -459,13 +459,12 @@ void V8ConsoleMessageStorage::addMessage(
V8InspectorImpl* inspector = m_inspector;
if (message->type() == ConsoleAPIType::kClear) clear();
V8InspectorSessionImpl* session =
inspector->sessionForContextGroup(contextGroupId);
if (session) {
if (message->origin() == V8MessageOrigin::kConsole)
session->consoleAgent()->messageAdded(message.get());
session->runtimeAgent()->messageAdded(message.get());
}
inspector->forEachSession(
contextGroupId, [&message](V8InspectorSessionImpl* session) {
if (message->origin() == V8MessageOrigin::kConsole)
session->consoleAgent()->messageAdded(message.get());
session->runtimeAgent()->messageAdded(message.get());
});
if (!inspector->hasConsoleMessageStorage(contextGroupId)) return;
DCHECK(m_messages.size() <= maxConsoleMessageCount);
......@@ -486,10 +485,10 @@ void V8ConsoleMessageStorage::addMessage(
void V8ConsoleMessageStorage::clear() {
m_messages.clear();
m_estimatedSize = 0;
if (V8InspectorSessionImpl* session =
m_inspector->sessionForContextGroup(m_contextGroupId)) {
session->releaseObjectGroup("console");
}
m_inspector->forEachSession(m_contextGroupId,
[](V8InspectorSessionImpl* session) {
session->releaseObjectGroup("console");
});
m_data.clear();
}
......
......@@ -124,24 +124,8 @@ class ConsoleHelper {
return func;
}
V8ProfilerAgentImpl* profilerAgent() {
if (V8InspectorSessionImpl* session = currentSession()) {
if (session && session->profilerAgent()->enabled())
return session->profilerAgent();
}
return nullptr;
}
V8DebuggerAgentImpl* debuggerAgent() {
if (V8InspectorSessionImpl* session = currentSession()) {
if (session && session->debuggerAgent()->enabled())
return session->debuggerAgent();
}
return nullptr;
}
V8InspectorSessionImpl* currentSession() {
return m_inspector->sessionForContextGroup(m_groupId);
void forEachSession(std::function<void(V8InspectorSessionImpl*)> callback) {
m_inspector->forEachSession(m_groupId, callback);
}
private:
......@@ -288,9 +272,12 @@ void V8Console::Assert(const v8::debug::ConsoleCallArguments& info) {
toV8String(m_inspector->isolate(), String16("console.assert")));
helper.reportCall(ConsoleAPIType::kAssert, arguments);
if (V8DebuggerAgentImpl* debuggerAgent = helper.debuggerAgent())
debuggerAgent->breakProgramOnException(
protocol::Debugger::Paused::ReasonEnum::Assert, nullptr);
helper.forEachSession([](V8InspectorSessionImpl* session) {
if (session->debuggerAgent()->enabled()) {
session->debuggerAgent()->breakProgramOnException(
protocol::Debugger::Paused::ReasonEnum::Assert, nullptr);
}
});
}
void V8Console::MarkTimeline(const v8::debug::ConsoleCallArguments& info) {
......@@ -304,14 +291,18 @@ void V8Console::MarkTimeline(const v8::debug::ConsoleCallArguments& info) {
void V8Console::Profile(const v8::debug::ConsoleCallArguments& info) {
ConsoleHelper helper(info, m_inspector);
if (V8ProfilerAgentImpl* profilerAgent = helper.profilerAgent())
profilerAgent->consoleProfile(helper.firstArgToString(String16()));
helper.forEachSession([&helper](V8InspectorSessionImpl* session) {
session->profilerAgent()->consoleProfile(
helper.firstArgToString(String16()));
});
}
void V8Console::ProfileEnd(const v8::debug::ConsoleCallArguments& info) {
ConsoleHelper helper(info, m_inspector);
if (V8ProfilerAgentImpl* profilerAgent = helper.profilerAgent())
profilerAgent->consoleProfileEnd(helper.firstArgToString(String16()));
helper.forEachSession([&helper](V8InspectorSessionImpl* session) {
session->profilerAgent()->consoleProfileEnd(
helper.firstArgToString(String16()));
});
}
static void timeFunction(const v8::debug::ConsoleCallArguments& info,
......@@ -426,20 +417,24 @@ static void setFunctionBreakpoint(ConsoleHelper& helper,
v8::Local<v8::Function> function,
V8DebuggerAgentImpl::BreakpointSource source,
const String16& condition, bool enable) {
V8DebuggerAgentImpl* debuggerAgent = helper.debuggerAgent();
if (!debuggerAgent) return;
String16 scriptId = String16::fromInteger(function->ScriptId());
int lineNumber = function->GetScriptLineNumber();
int columnNumber = function->GetScriptColumnNumber();
if (lineNumber == v8::Function::kLineOffsetNotFound ||
columnNumber == v8::Function::kLineOffsetNotFound)
return;
if (enable)
debuggerAgent->setBreakpointAt(scriptId, lineNumber, columnNumber, source,
condition);
else
debuggerAgent->removeBreakpointAt(scriptId, lineNumber, columnNumber,
source);
helper.forEachSession([&enable, &scriptId, &lineNumber, &columnNumber,
&source, &condition](V8InspectorSessionImpl* session) {
if (!session->debuggerAgent()->enabled()) return;
if (enable) {
session->debuggerAgent()->setBreakpointAt(
scriptId, lineNumber, columnNumber, source, condition);
} else {
session->debuggerAgent()->removeBreakpointAt(scriptId, lineNumber,
columnNumber, source);
}
});
}
void V8Console::debugFunctionCallback(
......@@ -526,10 +521,11 @@ static void inspectImpl(const v8::FunctionCallbackInfo<v8::Value>& info,
std::unique_ptr<protocol::DictionaryValue> hints =
protocol::DictionaryValue::create();
if (copyToClipboard) hints->setBoolean("copyToClipboard", true);
if (V8InspectorSessionImpl* session = helper.currentSession()) {
session->runtimeAgent()->inspect(std::move(wrappedObject),
std::move(hints));
}
helper.forEachSession(
[&wrappedObject, &hints](V8InspectorSessionImpl* session) {
session->runtimeAgent()->inspect(std::move(wrappedObject),
std::move(hints));
});
}
void V8Console::inspectCallback(
......@@ -546,14 +542,14 @@ void V8Console::inspectedObject(const v8::FunctionCallbackInfo<v8::Value>& info,
DCHECK(num < V8InspectorSessionImpl::kInspectedObjectBufferSize);
v8::debug::ConsoleCallArguments args(info);
ConsoleHelper helper(args, m_inspector);
if (V8InspectorSessionImpl* session = helper.currentSession()) {
helper.forEachSession([&info, &num](V8InspectorSessionImpl* session) {
V8InspectorSession::Inspectable* object = session->inspectedObject(num);
v8::Isolate* isolate = info.GetIsolate();
if (object)
info.GetReturnValue().Set(object->get(isolate->GetCurrentContext()));
else
info.GetReturnValue().Set(v8::Undefined(isolate));
}
});
}
void V8Console::installMemoryGetter(v8::Local<v8::Context> context,
......
......@@ -1238,7 +1238,15 @@ void V8DebuggerAgentImpl::breakProgram(
std::vector<BreakReason> currentScheduledReason;
currentScheduledReason.swap(m_breakReason);
pushBreakDetails(breakReason, std::move(data));
if (!m_debugger->breakProgram(m_session->contextGroupId())) return;
int contextGroupId = m_session->contextGroupId();
int sessionId = m_session->sessionId();
V8InspectorImpl* inspector = m_inspector;
m_debugger->breakProgram(contextGroupId);
// Check that session and |this| are still around.
if (!inspector->sessionById(contextGroupId, sessionId)) return;
if (!enabled()) return;
popBreakDetails();
m_breakReason.swap(currentScheduledReason);
if (!m_breakReason.empty()) {
......
......@@ -347,16 +347,13 @@ bool V8Debugger::canBreakProgram() {
return !v8::debug::AllFramesOnStackAreBlackboxed(m_isolate);
}
bool V8Debugger::breakProgram(int targetContextGroupId) {
void V8Debugger::breakProgram(int targetContextGroupId) {
// Don't allow nested breaks.
if (isPaused()) return true;
if (!canBreakProgram()) return true;
if (isPaused()) return;
if (!canBreakProgram()) return;
DCHECK(targetContextGroupId);
m_targetContextGroupId = targetContextGroupId;
v8::debug::BreakRightNow(m_isolate);
V8InspectorSessionImpl* session =
m_inspector->sessionForContextGroup(targetContextGroupId);
return session && session->debuggerAgent()->enabled();
}
void V8Debugger::continueProgram(int targetContextGroupId) {
......@@ -600,10 +597,19 @@ void V8Debugger::handleProgramBreak(v8::Local<v8::Context> pausedContext,
m_stepIntoAsyncCallback.reset();
}
m_breakRequested = false;
V8InspectorSessionImpl* session =
m_inspector->sessionForContextGroup(contextGroupId);
if (!session || !session->debuggerAgent()->enabled()) return;
if (!m_scheduledOOMBreak && session->debuggerAgent()->skipAllPauses()) return;
bool scheduledOOMBreak = m_scheduledOOMBreak;
auto agentCheck = [&scheduledOOMBreak](V8DebuggerAgentImpl* agent) {
return agent->enabled() && (scheduledOOMBreak || !agent->skipAllPauses());
};
bool hasAgents = false;
m_inspector->forEachSession(
contextGroupId,
[&agentCheck, &hasAgents](V8InspectorSessionImpl* session) {
if (agentCheck(session->debuggerAgent())) hasAgents = true;
});
if (!hasAgents) return;
std::vector<String16> breakpointIds;
if (!hitBreakpointNumbers.IsEmpty()) {
......@@ -627,9 +633,17 @@ void V8Debugger::handleProgramBreak(v8::Local<v8::Context> pausedContext,
m_pausedContext = pausedContext;
m_executionState = executionState;
m_pausedContextGroupId = contextGroupId;
session->debuggerAgent()->didPause(
InspectedContext::contextId(pausedContext), exception, breakpointIds,
isPromiseRejection, isUncaught, m_scheduledOOMBreak);
m_inspector->forEachSession(
contextGroupId, [&agentCheck, &pausedContext, &exception, &breakpointIds,
&isPromiseRejection, &isUncaught,
&scheduledOOMBreak](V8InspectorSessionImpl* session) {
if (agentCheck(session->debuggerAgent())) {
session->debuggerAgent()->didPause(
InspectedContext::contextId(pausedContext), exception,
breakpointIds, isPromiseRejection, isUncaught, scheduledOOMBreak);
}
});
{
v8::Context::Scope scope(pausedContext);
v8::Local<v8::Context> context = m_isolate->GetCurrentContext();
......@@ -638,10 +652,12 @@ void V8Debugger::handleProgramBreak(v8::Local<v8::Context> pausedContext,
m_inspector->client()->runMessageLoopOnPause(contextGroupId);
m_pausedContextGroupId = 0;
}
// The agent may have been removed in the nested loop.
session = m_inspector->sessionForContextGroup(contextGroupId);
if (session && session->debuggerAgent()->enabled())
session->debuggerAgent()->didContinue();
m_inspector->forEachSession(contextGroupId,
[](V8InspectorSessionImpl* session) {
if (session->debuggerAgent()->enabled())
session->debuggerAgent()->didContinue();
});
if (m_scheduledOOMBreak) m_isolate->RestoreOriginalHeapLimit();
m_scheduledOOMBreak = false;
m_pausedContext.Clear();
......@@ -662,16 +678,26 @@ void V8Debugger::ScriptCompiled(v8::Local<v8::debug::Script> script,
bool has_compile_error) {
int contextId;
if (!script->ContextId().To(&contextId)) return;
V8InspectorSessionImpl* session = m_inspector->sessionForContextGroup(
m_inspector->contextGroupId(contextId));
if (!session || !session->debuggerAgent()->enabled()) return;
if (script->IsWasm()) {
m_wasmTranslation.AddScript(script.As<v8::debug::WasmScript>(),
session->debuggerAgent());
WasmTranslation* wasmTranslation = &m_wasmTranslation;
m_inspector->forEachSession(
m_inspector->contextGroupId(contextId),
[&script, &wasmTranslation](V8InspectorSessionImpl* session) {
if (!session->debuggerAgent()->enabled()) return;
wasmTranslation->AddScript(script.As<v8::debug::WasmScript>(),
session->debuggerAgent());
});
} else if (m_ignoreScriptParsedEventsCounter == 0) {
session->debuggerAgent()->didParseSource(
V8DebuggerScript::Create(m_isolate, script, inLiveEditScope),
!has_compile_error);
v8::Isolate* isolate = m_isolate;
m_inspector->forEachSession(
m_inspector->contextGroupId(contextId),
[&isolate, &script,
&has_compile_error](V8InspectorSessionImpl* session) {
if (!session->debuggerAgent()->enabled()) return;
session->debuggerAgent()->didParseSource(
V8DebuggerScript::Create(isolate, script, inLiveEditScope),
!has_compile_error);
});
}
}
......@@ -704,11 +730,19 @@ bool V8Debugger::IsFunctionBlackboxed(v8::Local<v8::debug::Script> script,
const v8::debug::Location& end) {
int contextId;
if (!script->ContextId().To(&contextId)) return false;
V8InspectorSessionImpl* session = m_inspector->sessionForContextGroup(
m_inspector->contextGroupId(contextId));
if (!session || !session->debuggerAgent()->enabled()) return false;
return session->debuggerAgent()->isFunctionBlackboxed(
String16::fromInteger(script->Id()), start, end);
bool hasAgents = false;
bool allBlackboxed = true;
String16 scriptId = String16::fromInteger(script->Id());
m_inspector->forEachSession(
m_inspector->contextGroupId(contextId),
[&hasAgents, &allBlackboxed, &scriptId, &start,
&end](V8InspectorSessionImpl* session) {
V8DebuggerAgentImpl* agent = session->debuggerAgent();
if (!agent->enabled()) return;
hasAgents = true;
allBlackboxed &= agent->isFunctionBlackboxed(scriptId, start, end);
});
return hasAgents && allBlackboxed;
}
void V8Debugger::PromiseEventOccurred(v8::debug::PromiseDebugActionType type,
......@@ -1084,10 +1118,14 @@ std::unique_ptr<V8StackTraceImpl> V8Debugger::captureStackTrace(
if (!contextGroupId) return nullptr;
int stackSize = 1;
V8InspectorSessionImpl* session =
m_inspector->sessionForContextGroup(contextGroupId);
if (fullStack || (session && session->runtimeAgent()->enabled())) {
if (fullStack) {
stackSize = V8StackTraceImpl::maxCallStackSizeToCapture;
} else {
m_inspector->forEachSession(
contextGroupId, [&stackSize](V8InspectorSessionImpl* session) {
if (session->runtimeAgent()->enabled())
stackSize = V8StackTraceImpl::maxCallStackSizeToCapture;
});
}
return V8StackTraceImpl::capture(this, contextGroupId, stackSize);
}
......
......@@ -50,7 +50,7 @@ class V8Debugger : public v8::debug::DebugDelegate {
v8::debug::ExceptionBreakState getPauseOnExceptionsState();
void setPauseOnExceptionsState(v8::debug::ExceptionBreakState);
bool canBreakProgram();
bool breakProgram(int targetContextGroupId);
void breakProgram(int targetContextGroupId);
void continueProgram(int targetContextGroupId);
void setPauseOnNextStatement(bool, int targetContextGroupId);
......
......@@ -151,20 +151,18 @@ std::unique_ptr<V8StackTrace> V8InspectorImpl::createStackTrace(
std::unique_ptr<V8InspectorSession> V8InspectorImpl::connect(
int contextGroupId, V8Inspector::Channel* channel,
const StringView& state) {
DCHECK(m_sessions.find(contextGroupId) == m_sessions.cend());
int sessionId = ++m_lastSessionId;
std::unique_ptr<V8InspectorSessionImpl> session =
V8InspectorSessionImpl::create(this, contextGroupId, sessionId, channel,
state);
m_sessions[contextGroupId] = session.get();
m_sessionById[sessionId] = session.get();
m_sessions[contextGroupId][sessionId] = session.get();
return std::move(session);
}
void V8InspectorImpl::disconnect(V8InspectorSessionImpl* session) {
DCHECK(m_sessions.find(session->contextGroupId()) != m_sessions.end());
m_sessions.erase(session->contextGroupId());
m_sessionById.erase(session->sessionId());
auto& map = m_sessions[session->contextGroupId()];
map.erase(session->sessionId());
if (map.empty()) m_sessions.erase(session->contextGroupId());
}
InspectedContext* V8InspectorImpl::getContext(int groupId,
......@@ -196,9 +194,10 @@ void V8InspectorImpl::contextCreated(const V8ContextInfo& info) {
DCHECK(contextById->find(contextId) == contextById->cend());
(*contextById)[contextId].reset(context);
SessionMap::iterator sessionIt = m_sessions.find(info.contextGroupId);
if (sessionIt != m_sessions.end())
sessionIt->second->runtimeAgent()->reportExecutionContextCreated(context);
forEachSession(
info.contextGroupId, [&context](V8InspectorSessionImpl* session) {
session->runtimeAgent()->reportExecutionContextCreated(context);
});
}
void V8InspectorImpl::contextDestroyed(v8::Local<v8::Context> context) {
......@@ -213,31 +212,34 @@ void V8InspectorImpl::contextDestroyed(v8::Local<v8::Context> context) {
InspectedContext* inspectedContext = getContext(groupId, contextId);
if (!inspectedContext) return;
SessionMap::iterator iter = m_sessions.find(groupId);
if (iter != m_sessions.end())
iter->second->runtimeAgent()->reportExecutionContextDestroyed(
inspectedContext);
forEachSession(groupId, [&inspectedContext](V8InspectorSessionImpl* session) {
session->runtimeAgent()->reportExecutionContextDestroyed(inspectedContext);
});
discardInspectedContext(groupId, contextId);
}
void V8InspectorImpl::resetContextGroup(int contextGroupId) {
m_consoleStorageMap.erase(contextGroupId);
m_muteExceptionsMap.erase(contextGroupId);
SessionMap::iterator session = m_sessions.find(contextGroupId);
if (session != m_sessions.end()) session->second->reset();
forEachSession(contextGroupId,
[](V8InspectorSessionImpl* session) { session->reset(); });
m_contexts.erase(contextGroupId);
m_debugger->wasmTranslation()->Clear();
}
void V8InspectorImpl::idleStarted() {
for (auto it = m_sessions.begin(); it != m_sessions.end(); ++it) {
if (it->second->profilerAgent()->idleStarted()) return;
for (auto& it : m_sessions) {
for (auto& it2 : it.second) {
if (it2.second->profilerAgent()->idleStarted()) return;
}
}
}
void V8InspectorImpl::idleFinished() {
for (auto it = m_sessions.begin(); it != m_sessions.end(); ++it) {
if (it->second->profilerAgent()->idleFinished()) return;
for (auto& it : m_sessions) {
for (auto& it2 : it.second) {
if (it2.second->profilerAgent()->idleFinished()) return;
}
}
}
......@@ -312,16 +314,12 @@ void V8InspectorImpl::discardInspectedContext(int contextGroupId,
if (m_contexts[contextGroupId]->empty()) m_contexts.erase(contextGroupId);
}
V8InspectorSessionImpl* V8InspectorImpl::sessionForContextGroup(
int contextGroupId) {
if (!contextGroupId) return nullptr;
SessionMap::iterator iter = m_sessions.find(contextGroupId);
return iter == m_sessions.end() ? nullptr : iter->second;
}
V8InspectorSessionImpl* V8InspectorImpl::sessionById(int sessionId) {
auto it = m_sessionById.find(sessionId);
return it == m_sessionById.end() ? nullptr : it->second;
V8InspectorSessionImpl* V8InspectorImpl::sessionById(int contextGroupId,
int sessionId) {
auto it = m_sessions.find(contextGroupId);
if (it == m_sessions.end()) return nullptr;
auto it2 = it->second.find(sessionId);
return it2 == it->second.end() ? nullptr : it2->second;
}
V8Console* V8InspectorImpl::console() {
......@@ -346,4 +344,21 @@ void V8InspectorImpl::forEachContext(
}
}
void V8InspectorImpl::forEachSession(
int contextGroupId, std::function<void(V8InspectorSessionImpl*)> callback) {
auto it = m_sessions.find(contextGroupId);
if (it == m_sessions.end()) return;
std::vector<int> ids;
ids.reserve(it->second.size());
for (auto& sessionIt : it->second) ids.push_back(sessionIt.first);
// Retrieve by ids each time since |callback| may destroy some contexts.
for (auto& sessionId : ids) {
it = m_sessions.find(contextGroupId);
if (it == m_sessions.end()) continue;
auto sessionIt = it->second.find(sessionId);
if (sessionIt != it->second.end()) callback(sessionIt->second);
}
}
} // namespace v8_inspector
......@@ -104,12 +104,13 @@ class V8InspectorImpl : public V8Inspector {
bool hasConsoleMessageStorage(int contextGroupId);
void discardInspectedContext(int contextGroupId, int contextId);
void disconnect(V8InspectorSessionImpl*);
V8InspectorSessionImpl* sessionForContextGroup(int contextGroupId);
V8InspectorSessionImpl* sessionById(int sessionId);
V8InspectorSessionImpl* sessionById(int contextGroupId, int sessionId);
InspectedContext* getContext(int groupId, int contextId) const;
V8Console* console();
void forEachContext(int contextGroupId,
std::function<void(InspectedContext*)> callback);
void forEachSession(int contextGroupId,
std::function<void(V8InspectorSessionImpl*)> callback);
private:
v8::Isolate* m_isolate;
......@@ -130,9 +131,9 @@ class V8InspectorImpl : public V8Inspector {
protocol::HashMap<int, std::unique_ptr<ContextByIdMap>>;
ContextsByGroupMap m_contexts;
using SessionMap = protocol::HashMap<int, V8InspectorSessionImpl*>;
SessionMap m_sessions;
protocol::HashMap<int, V8InspectorSessionImpl*> m_sessionById;
// contextGroupId -> sessionId -> session
protocol::HashMap<int, protocol::HashMap<int, V8InspectorSessionImpl*>>
m_sessions;
using ConsoleStorageMap =
protocol::HashMap<int, std::unique_ptr<V8ConsoleMessageStorage>>;
......
......@@ -161,6 +161,7 @@ class ProtocolPromiseHandler {
std::unique_ptr<Callback> callback)
: m_inspector(session->inspector()),
m_sessionId(session->sessionId()),
m_contextGroupId(session->contextGroupId()),
m_executionContextId(executionContextId),
m_objectGroup(objectGroup),
m_returnByValue(returnByValue),
......@@ -185,7 +186,8 @@ class ProtocolPromiseHandler {
std::unique_ptr<protocol::Runtime::RemoteObject> wrapObject(
v8::Local<v8::Value> value) {
V8InspectorSessionImpl* session = m_inspector->sessionById(m_sessionId);
V8InspectorSessionImpl* session =
m_inspector->sessionById(m_contextGroupId, m_sessionId);
if (!session) {
m_callback->sendFailure(Response::Error("No session"));
return nullptr;
......@@ -209,6 +211,7 @@ class ProtocolPromiseHandler {
V8InspectorImpl* m_inspector;
int m_sessionId;
int m_contextGroupId;
int m_executionContextId;
String16 m_objectGroup;
bool m_returnByValue;
......@@ -696,9 +699,11 @@ Response V8RuntimeAgentImpl::disable() {
void V8RuntimeAgentImpl::reset() {
m_compiledScripts.clear();
if (m_enabled) {
m_inspector->forEachContext(
m_session->contextGroupId(),
[](InspectedContext* context) { context->setReported(false); });
int sessionId = m_session->sessionId();
m_inspector->forEachContext(m_session->contextGroupId(),
[&sessionId](InspectedContext* context) {
context->setReported(sessionId, false);
});
m_frontend.executionContextsCleared();
}
}
......@@ -706,7 +711,7 @@ void V8RuntimeAgentImpl::reset() {
void V8RuntimeAgentImpl::reportExecutionContextCreated(
InspectedContext* context) {
if (!m_enabled) return;
context->setReported(true);
context->setReported(m_session->sessionId(), true);
std::unique_ptr<protocol::Runtime::ExecutionContextDescription> description =
protocol::Runtime::ExecutionContextDescription::create()
.setId(context->contextId())
......@@ -721,8 +726,8 @@ void V8RuntimeAgentImpl::reportExecutionContextCreated(
void V8RuntimeAgentImpl::reportExecutionContextDestroyed(
InspectedContext* context) {
if (m_enabled && context->isReported()) {
context->setReported(false);
if (m_enabled && context->isReported(m_session->sessionId())) {
context->setReported(m_session->sessionId(), false);
m_frontend.executionContextDestroyed(context->contextId());
}
}
......
Tests that Debugger.setSkipAllPauses skips breaks and does not block resumed notifications
paused at:
#debugger;
paused at:
#debugger
// Copyright 2017 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.
let {session, contextGroup, Protocol} = InspectorTest.start('Tests that Debugger.setSkipAllPauses skips breaks and does not block resumed notifications');
session.setupScriptMap();
(async function test() {
await Protocol.Debugger.enable();
Protocol.Runtime.evaluate({expression: 'debugger;'});
await waitForPause();
await Protocol.Debugger.resume();
await Protocol.Debugger.setSkipAllPauses({skip: true});
await Protocol.Runtime.evaluate({expression: 'debugger'});
await Protocol.Debugger.setSkipAllPauses({skip: false});
Protocol.Runtime.evaluate({expression: 'debugger'});
await waitForPause();
Protocol.Debugger.setSkipAllPauses({skip: true});
Protocol.Debugger.resume();
await Protocol.Debugger.onceResumed();
InspectorTest.completeTest();
})();
async function waitForPause() {
var message = await Protocol.Debugger.oncePaused();
InspectorTest.log('paused at:');
session.logSourceLocation(message.params.callFrames[0].location);
}
......@@ -54,6 +54,7 @@ IsolateData::IsolateData(TaskRunner* task_runner,
isolate_->SetMicrotasksPolicy(v8::MicrotasksPolicy::kScoped);
if (with_inspector) {
isolate_->AddMessageListener(&IsolateData::MessageHandler);
isolate_->SetPromiseRejectCallback(&IsolateData::PromiseRejectHandler);
inspector_ = v8_inspector::V8Inspector::create(isolate_, this);
}
}
......@@ -191,11 +192,11 @@ void IsolateData::DumpAsyncTaskStacksStateForTest() {
}
// static
void IsolateData::MessageHandler(v8::Local<v8::Message> message,
v8::Local<v8::Value> exception) {
int IsolateData::HandleMessage(v8::Local<v8::Message> message,
v8::Local<v8::Value> exception) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Context> context = isolate->GetEnteredContext();
if (context.IsEmpty()) return;
if (context.IsEmpty()) return 0;
v8_inspector::V8Inspector* inspector =
IsolateData::FromContext(context)->inspector_.get();
......@@ -222,9 +223,51 @@ void IsolateData::MessageHandler(v8::Local<v8::Message> message,
}
v8_inspector::StringView url(url_string.start(), url_string.length());
inspector->exceptionThrown(context, message_text, exception, detailed_message,
url, line_number, column_number,
inspector->createStackTrace(stack), script_id);
return inspector->exceptionThrown(
context, message_text, exception, detailed_message, url, line_number,
column_number, inspector->createStackTrace(stack), script_id);
}
// static
void IsolateData::MessageHandler(v8::Local<v8::Message> message,
v8::Local<v8::Value> exception) {
HandleMessage(message, exception);
}
// static
void IsolateData::PromiseRejectHandler(v8::PromiseRejectMessage data) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::Local<v8::Context> context = isolate->GetEnteredContext();
if (context.IsEmpty()) return;
v8::Local<v8::Promise> promise = data.GetPromise();
v8::Local<v8::Private> id_private = v8::Private::ForApi(
isolate,
v8::String::NewFromUtf8(isolate, "id", v8::NewStringType::kNormal)
.ToLocalChecked());
if (data.GetEvent() == v8::kPromiseHandlerAddedAfterReject) {
v8::Local<v8::Value> id;
if (!promise->GetPrivate(context, id_private).ToLocal(&id)) return;
if (!id->IsInt32()) return;
v8_inspector::V8Inspector* inspector =
IsolateData::FromContext(context)->inspector_.get();
const char* reason_str = "Handler added to rejected promise";
inspector->exceptionRevoked(
context, id.As<v8::Int32>()->Value(),
v8_inspector::StringView(reinterpret_cast<const uint8_t*>(reason_str),
strlen(reason_str)));
return;
}
v8::Local<v8::Value> exception = data.GetValue();
int exception_id = HandleMessage(
v8::Exception::CreateMessage(isolate, exception), exception);
if (exception_id) {
promise
->SetPrivate(isolate->GetCurrentContext(), id_private,
v8::Int32::New(isolate, exception_id))
.ToChecked();
}
}
void IsolateData::FireContextCreated(v8::Local<v8::Context> context,
......
......@@ -83,6 +83,9 @@ class IsolateData : public v8_inspector::V8InspectorClient {
v8::Local<v8::Module> referrer);
static void MessageHandler(v8::Local<v8::Message> message,
v8::Local<v8::Value> exception);
static void PromiseRejectHandler(v8::PromiseRejectMessage data);
static int HandleMessage(v8::Local<v8::Message> message,
v8::Local<v8::Value> exception);
std::vector<int> GetSessionIds(int context_group_id);
// V8InspectorClient implementation.
......
Tests that creating multiple sessions works.
Connecting session 1
From session 1
{
method : Runtime.executionContextCreated
params : {
context : {
id : 1
name :
origin :
}
}
}
Connecting session 2
From session 2
{
method : Runtime.executionContextCreated
params : {
context : {
id : 1
name :
origin :
}
}
}
Reconnecting session 2
From session 2
{
method : Runtime.executionContextCreated
params : {
context : {
id : 1
name :
origin :
}
}
}
Reconnecting session 1
From session 1
{
method : Runtime.executionContextCreated
params : {
context : {
id : 1
name :
origin :
}
}
}
Connecting session 3
From session 3
{
method : Runtime.executionContextCreated
params : {
context : {
id : 1
name :
origin :
}
}
}
Destroying and creating context
From session 3
{
method : Runtime.executionContextDestroyed
params : {
executionContextId : <executionContextId>
}
}
id matching: true
From session 1
{
method : Runtime.executionContextDestroyed
params : {
executionContextId : <executionContextId>
}
}
id matching: true
From session 2
{
method : Runtime.executionContextDestroyed
params : {
executionContextId : <executionContextId>
}
}
id matching: true
From session 3
{
method : Runtime.executionContextCreated
params : {
context : {
id : 2
name :
origin :
}
}
}
From session 1
{
method : Runtime.executionContextCreated
params : {
context : {
id : 2
name :
origin :
}
}
}
From session 2
{
method : Runtime.executionContextCreated
params : {
context : {
id : 2
name :
origin :
}
}
}
Disconnecting all sessions
Connecting session 4
From session 4
{
method : Runtime.executionContextCreated
params : {
context : {
id : 2
name :
origin :
}
}
}
// Copyright 2017 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.
InspectorTest.log('Tests that creating multiple sessions works.');
function connect(contextGroup, num) {
var session = contextGroup.connect();
var executionContextId;
session.Protocol.Runtime.onExecutionContextCreated(message => {
InspectorTest.log('From session ' + num);
InspectorTest.logMessage(message);
executionContextId = message.params.context.id;
});
session.Protocol.Runtime.onExecutionContextDestroyed(message => {
InspectorTest.log('From session ' + num);
InspectorTest.logMessage(message);
InspectorTest.log('id matching: ' + (message.params.executionContextId === executionContextId));
});
return session;
}
(async function test() {
var contextGroup = new InspectorTest.ContextGroup();
InspectorTest.log('Connecting session 1');
var session1 = connect(contextGroup, 1);
await session1.Protocol.Runtime.enable();
InspectorTest.log('Connecting session 2');
var session2 = connect(contextGroup, 2);
await session2.Protocol.Runtime.enable();
InspectorTest.log('Reconnecting session 2');
session2.reconnect();
await session2.Protocol.Runtime.enable();
InspectorTest.log('Reconnecting session 1');
session1.reconnect();
await session1.Protocol.Runtime.enable();
InspectorTest.log('Connecting session 3');
var session3 = connect(contextGroup, 3);
await session3.Protocol.Runtime.enable();
InspectorTest.log('Destroying and creating context');
await session2.Protocol.Runtime.evaluate({expression: 'inspector.fireContextDestroyed(); inspector.fireContextCreated(); '});
InspectorTest.log('Disconnecting all sessions');
session1.disconnect();
session2.disconnect();
session3.disconnect();
InspectorTest.log('Connecting session 4');
var session4 = connect(contextGroup, 4);
await session4.Protocol.Runtime.enable();
InspectorTest.completeTest();
})();
Tests that all sessions get console api notifications.
Error in 2
From session 2
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
description : 1
type : number
value : 1
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 8
functionName :
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : error
}
}
From session 1
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
description : 1
type : number
value : 1
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 8
functionName :
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : error
}
}
Logging in 1
From session 2
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
description : 2
type : number
value : 2
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 8
functionName :
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : log
}
}
From session 1
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
description : 2
type : number
value : 2
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 8
functionName :
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : log
}
}
Error in setTimeout 1
From session 2
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : a
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 25
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : error
}
}
From session 1
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : a
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 25
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : error
}
}
Logging in setTimeout 2
From session 2
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : b
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 25
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : log
}
}
From session 1
{
method : Runtime.consoleAPICalled
params : {
args : [
[0] : {
type : string
value : b
}
]
executionContextId : <executionContextId>
stackTrace : {
callFrames : [
[0] : {
columnNumber : 25
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
timestamp : <timestamp>
type : log
}
}
// Copyright 2017 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.
InspectorTest.log('Tests that all sessions get console api notifications.');
function connect(contextGroup, num) {
var session = contextGroup.connect();
session.Protocol.Runtime.onConsoleAPICalled(message => {
InspectorTest.log('From session ' + num);
InspectorTest.logMessage(message);
});
return session;
}
(async function test() {
var contextGroup = new InspectorTest.ContextGroup();
var session1 = connect(contextGroup, 1);
var session2 = connect(contextGroup, 2);
await session1.Protocol.Runtime.enable();
await session2.Protocol.Runtime.enable();
InspectorTest.log('Error in 2');
await session2.Protocol.Runtime.evaluate({expression: 'console.error(1)'});
InspectorTest.log('Logging in 1');
await session1.Protocol.Runtime.evaluate({expression: 'console.log(2)'});
InspectorTest.log('Error in setTimeout 1');
await session1.Protocol.Runtime.evaluate({expression: 'setTimeout(() => console.error("a"), 0)'});
await InspectorTest.waitForPendingTasks();
InspectorTest.log('Logging in setTimeout 2');
await session2.Protocol.Runtime.evaluate({expression: 'setTimeout(() => console.log("b"), 0)'});
await InspectorTest.waitForPendingTasks();
InspectorTest.completeTest();
})();
Tests that all sessions get exception notifications.
Throwing in 2
Throwing in 1
Throwing in setTimeout 1
From session 2
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 19
exception : {
type : string
value : error3
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 19
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error3
}
timestamp : <timestamp>
}
}
From session 1
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 19
exception : {
type : string
value : error3
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 19
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error3
}
timestamp : <timestamp>
}
}
Throwing in setTimeout 2
From session 2
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 19
exception : {
type : string
value : error4
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 19
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error4
}
timestamp : <timestamp>
}
}
From session 1
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 19
exception : {
type : string
value : error4
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 19
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error4
}
timestamp : <timestamp>
}
}
Rejecting in 2
From session 2
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 40
exception : {
type : string
value : error5
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 40
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error5
}
timestamp : <timestamp>
}
}
From session 1
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 40
exception : {
type : string
value : error5
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 40
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error5
}
timestamp : <timestamp>
}
}
Revoking in 2
From session 2
{
method : Runtime.exceptionRevoked
params : {
exceptionId : <exceptionId>
reason : Handler added to rejected promise
}
}
id matching: true
From session 1
{
method : Runtime.exceptionRevoked
params : {
exceptionId : <exceptionId>
reason : Handler added to rejected promise
}
}
id matching: true
Rejecting in 1
From session 2
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 40
exception : {
type : string
value : error6
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 40
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error6
}
timestamp : <timestamp>
}
}
From session 1
{
method : Runtime.exceptionThrown
params : {
exceptionDetails : {
columnNumber : 40
exception : {
type : string
value : error6
}
exceptionId : <exceptionId>
executionContextId : <executionContextId>
lineNumber : 0
stackTrace : {
callFrames : [
[0] : {
columnNumber : 40
functionName : setTimeout
lineNumber : 0
scriptId : <scriptId>
url :
}
]
}
text : Uncaught error6
}
timestamp : <timestamp>
}
}
Revoking in 1
From session 2
{
method : Runtime.exceptionRevoked
params : {
exceptionId : <exceptionId>
reason : Handler added to rejected promise
}
}
id matching: true
From session 1
{
method : Runtime.exceptionRevoked
params : {
exceptionId : <exceptionId>
reason : Handler added to rejected promise
}
}
id matching: true
// Copyright 2017 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.
InspectorTest.log('Tests that all sessions get exception notifications.');
function connect(contextGroup, num) {
var session = contextGroup.connect();
var exceptionId;
session.Protocol.Runtime.onExceptionThrown(message => {
InspectorTest.log('From session ' + num);
InspectorTest.logMessage(message);
exceptionId = message.params.exceptionDetails.exceptionId;
});
session.Protocol.Runtime.onExceptionRevoked(message => {
InspectorTest.log('From session ' + num);
InspectorTest.logMessage(message);
InspectorTest.log('id matching: ' + (message.params.exceptionId === exceptionId));
});
return session;
}
(async function test() {
var contextGroup = new InspectorTest.ContextGroup();
var session1 = connect(contextGroup, 1);
var session2 = connect(contextGroup, 2);
await session1.Protocol.Runtime.enable();
await session2.Protocol.Runtime.enable();
InspectorTest.log('Throwing in 2');
await session2.Protocol.Runtime.evaluate({expression: 'throw "error1";'});
InspectorTest.log('Throwing in 1');
await session1.Protocol.Runtime.evaluate({expression: 'throw "error2";'});
InspectorTest.log('Throwing in setTimeout 1');
await session1.Protocol.Runtime.evaluate({expression: 'setTimeout(() => { throw "error3"; }, 0)'});
await InspectorTest.waitForPendingTasks();
InspectorTest.log('Throwing in setTimeout 2');
await session2.Protocol.Runtime.evaluate({expression: 'setTimeout(() => { throw "error4"; }, 0)'});
await InspectorTest.waitForPendingTasks();
InspectorTest.log('Rejecting in 2');
await session2.Protocol.Runtime.evaluate({expression: 'var p2; setTimeout(() => { p2 = Promise.reject("error5") }, 0)'});
await InspectorTest.waitForPendingTasks();
InspectorTest.log('Revoking in 2');
await session2.Protocol.Runtime.evaluate({expression: 'setTimeout(() => { p2.catch() }, 0);'});
await InspectorTest.waitForPendingTasks();
InspectorTest.log('Rejecting in 1');
await session1.Protocol.Runtime.evaluate({expression: 'var p1; setTimeout(() => { p1 = Promise.reject("error6")} , 0)'});
await InspectorTest.waitForPendingTasks();
InspectorTest.log('Revoking in 1');
await session1.Protocol.Runtime.evaluate({expression: 'setTimeout(() => { p1.catch() }, 0);'});
await InspectorTest.waitForPendingTasks();
InspectorTest.completeTest();
})();
Tests that multiple sessions share the context.
Assigning in 1
Evaluating in 2
{
id : <messageId>
result : {
result : {
description : 42
type : number
value : 42
}
}
}
Awaiting in 1
Resolving in 2
Should resolve in 1
{
id : <messageId>
result : {
result : {
type : string
value : foo
}
}
}
// Copyright 2017 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.
InspectorTest.log('Tests that multiple sessions share the context.');
(async function test() {
var contextGroup = new InspectorTest.ContextGroup();
var session1 = contextGroup.connect();
var session2 = contextGroup.connect();
InspectorTest.log('Assigning in 1');
await session1.Protocol.Runtime.evaluate({expression: 'var a = 42;'});
InspectorTest.log('Evaluating in 2');
InspectorTest.logMessage(await session2.Protocol.Runtime.evaluate({expression: 'a'}));
InspectorTest.log('Awaiting in 1');
var promise = session1.Protocol.Runtime.evaluate({expression: 'var cb; new Promise(f => cb = f)', awaitPromise: true});
InspectorTest.log('Resolving in 2');
await session2.Protocol.Runtime.evaluate({expression: 'cb("foo")'});
InspectorTest.log('Should resolve in 1');
InspectorTest.logMessage(await promise);
InspectorTest.completeTest();
})();
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment