| // Copyright 2016 Google Inc. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include "cobalt/debug/javascript_debugger_component.h" |
| |
| #include "base/bind.h" |
| #include "base/optional.h" |
| #include "base/stringprintf.h" |
| #include "base/values.h" |
| |
| namespace cobalt { |
| namespace debug { |
| |
| namespace { |
| // Command, parameter and event names as specified by the protocol: |
| // https://developer.chrome.com/devtools/docs/protocol/1.1/debugger |
| |
| // Commands. |
| const char kDisable[] = "Debugger.disable"; |
| const char kEnable[] = "Debugger.enable"; |
| const char kGetScriptSource[] = "Debugger.getScriptSource"; |
| const char kPause[] = "Debugger.pause"; |
| const char kResume[] = "Debugger.resume"; |
| const char kSetBreakpointByUrl[] = "Debugger.setBreakpointByUrl"; |
| const char kSetPauseOnExceptions[] = "Debugger.setPauseOnExceptions"; |
| const char kStepInto[] = "Debugger.stepInto"; |
| const char kStepOut[] = "Debugger.stepOut"; |
| const char kStepOver[] = "Debugger.stepOver"; |
| |
| // Parameter names. |
| const char kCallFrameId[] = "callFrameId"; |
| const char kCallFrames[] = "callFrames"; |
| const char kColumnNumber[] = "columnNumber"; |
| const char kErrorLine[] = "errorLine"; |
| const char kErrorMessage[] = "errorMessage"; |
| const char kFunctionName[] = "functionName"; |
| const char kLineNumber[] = "lineNumber"; |
| const char kLocationColumnNumber[] = "location.columnNumber"; |
| const char kLocationLineNumber[] = "location.lineNumber"; |
| const char kLocationScriptId[] = "location.scriptId"; |
| const char kObject[] = "object"; |
| const char kReason[] = "reason"; |
| const char kScopeChain[] = "scopeChain"; |
| const char kScriptId[] = "scriptId"; |
| const char kState[] = "state"; |
| const char kThis[] = "this"; |
| const char kType[] = "type"; |
| const char kUrl[] = "url"; |
| |
| // Parameter values. |
| const char kDebugCommand[] = "debugCommand"; |
| |
| // Result parameters. |
| const char kBreakpointId[] = "result.breakpointId"; |
| const char kLocations[] = "result.locations"; |
| const char kScriptSource[] = "result.scriptSource"; |
| |
| // Events. |
| const char kPaused[] = "Debugger.paused"; |
| const char kResumed[] = "Debugger.resumed"; |
| const char kScriptFailedToParse[] = "Debugger.scriptFailedToParse"; |
| const char kScriptParsed[] = "Debugger.scriptParsed"; |
| |
| // Construct a unique breakpoint id from url and source location. |
| // Use the same format as Chrome. |
| std::string BreakpointId(const std::string url, int line_number, |
| int column_number) { |
| return base::StringPrintf("%s:%d:%d", url.c_str(), line_number, |
| column_number); |
| } |
| |
| } // namespace |
| |
| JavaScriptDebuggerComponent::JavaScriptDebuggerComponent( |
| ComponentConnector* connector) |
| : connector_(connector), source_providers_deleter_(&source_providers_) { |
| DCHECK(connector_); |
| connector_->AddCommand(kDisable, |
| base::Bind(&JavaScriptDebuggerComponent::Disable, |
| base::Unretained(this))); |
| connector_->AddCommand( |
| kEnable, |
| base::Bind(&JavaScriptDebuggerComponent::Enable, base::Unretained(this))); |
| connector_->AddCommand( |
| kGetScriptSource, |
| base::Bind(&JavaScriptDebuggerComponent::GetScriptSource, |
| base::Unretained(this))); |
| connector_->AddCommand(kPause, base::Bind(&JavaScriptDebuggerComponent::Pause, |
| base::Unretained(this))); |
| connector_->AddCommand( |
| kResume, |
| base::Bind(&JavaScriptDebuggerComponent::Resume, base::Unretained(this))); |
| connector_->AddCommand(kStepInto, |
| base::Bind(&JavaScriptDebuggerComponent::StepInto, |
| base::Unretained(this))); |
| connector_->AddCommand( |
| kSetBreakpointByUrl, |
| base::Bind(&JavaScriptDebuggerComponent::SetBreakpointByUrl, |
| base::Unretained(this))); |
| connector_->AddCommand( |
| kSetPauseOnExceptions, |
| base::Bind(&JavaScriptDebuggerComponent::SetPauseOnExceptions, |
| base::Unretained(this))); |
| connector_->AddCommand(kStepOut, |
| base::Bind(&JavaScriptDebuggerComponent::StepOut, |
| base::Unretained(this))); |
| connector_->AddCommand(kStepOver, |
| base::Bind(&JavaScriptDebuggerComponent::StepOver, |
| base::Unretained(this))); |
| } |
| |
| JavaScriptDebuggerComponent::~JavaScriptDebuggerComponent() {} |
| |
| JSONObject JavaScriptDebuggerComponent::Enable(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| connector_->script_debugger()->Attach(); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::Disable(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| connector_->script_debugger()->Detach(); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::GetScriptSource( |
| const JSONObject& params) { |
| // Get the scriptId from the parameters. |
| std::string script_id; |
| bool got_script_id = params->GetString(kScriptId, &script_id); |
| if (!got_script_id) { |
| return connector_->ErrorResponse("No scriptId specified in parameters."); |
| } |
| |
| // Find the source provider with a matching scriptId. |
| SourceProviderMap::iterator it = source_providers_.find(script_id); |
| if (it == source_providers_.end()) { |
| return connector_->ErrorResponse( |
| "No script found with specified scriptId."); |
| } |
| script::SourceProvider* source_provider = it->second; |
| DCHECK(source_provider); |
| |
| // Build and return the result. |
| JSONObject result(new base::DictionaryValue()); |
| result->SetString(kScriptSource, source_provider->GetScriptSource()); |
| return result.Pass(); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::Pause(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| connector_->script_debugger()->Pause(); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::Resume(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| DCHECK(connector_->server()); |
| connector_->script_debugger()->Resume(); |
| connector_->server()->SetPaused(false); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::SetBreakpointByUrl( |
| const JSONObject& params) { |
| DCHECK(connector_->script_debugger()); |
| DCHECK(connector_->server()); |
| |
| std::string url; |
| bool got_url = params->GetString(kUrl, &url); |
| if (!got_url) { |
| return connector_->ErrorResponse("Breakpoint URL must be specified."); |
| } |
| |
| // TODO: Should also handle setting of breakpoint by urlRegex |
| |
| int line_number; |
| bool got_line_number = params->GetInteger(kLineNumber, &line_number); |
| if (!got_line_number) { |
| return connector_->ErrorResponse("Line number must be specified."); |
| } |
| // If no column number is specified, just default to 0. |
| int column_number = 0; |
| params->GetInteger(kColumnNumber, &column_number); |
| |
| // TODO: Should also handle condition and isAntibreakpoint. |
| |
| // Create a new logical breakpoint and store it in our map. |
| const std::string breakpoint_id = |
| BreakpointId(url, line_number, column_number); |
| Breakpoint breakpoint(url, line_number, column_number); |
| breakpoints_[breakpoint_id] = breakpoint; |
| |
| // Check the logical breakpoint against all currently loaded source providers |
| // and get an array of matching breakpoint locations. |
| std::vector<ScriptLocation> locations; |
| ResolveBreakpoint(breakpoint, &locations); |
| |
| // Construct a result object from the logical breakpoint id and resolved |
| // source locations. |
| JSONObject result(new base::DictionaryValue()); |
| result->SetString(kBreakpointId, breakpoint_id); |
| JSONList location_objects(new base::ListValue()); |
| for (std::vector<ScriptLocation>::const_iterator it = locations.begin(); |
| it != locations.end(); ++it) { |
| JSONObject location(new base::DictionaryValue()); |
| location->SetString(kScriptId, it->script_id); |
| location->SetInteger(kLineNumber, it->line_number); |
| location->SetInteger(kColumnNumber, it->column_number); |
| location_objects->Append(location.release()); |
| } |
| result->Set(kLocations, location_objects.release()); |
| return result.Pass(); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::SetPauseOnExceptions( |
| const JSONObject& params) { |
| DCHECK(connector_->script_debugger()); |
| DCHECK(connector_->server()); |
| |
| std::string state; |
| DCHECK(params->GetString(kState, &state)); |
| if (state == "all") { |
| connector_->script_debugger()->SetPauseOnExceptions( |
| script::ScriptDebugger::kAll); |
| } else if (state == "none") { |
| connector_->script_debugger()->SetPauseOnExceptions( |
| script::ScriptDebugger::kNone); |
| } else if (state == "uncaught") { |
| connector_->script_debugger()->SetPauseOnExceptions( |
| script::ScriptDebugger::kUncaught); |
| } else { |
| NOTREACHED(); |
| } |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::StepInto(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| DCHECK(connector_->server()); |
| connector_->script_debugger()->StepInto(); |
| connector_->server()->SetPaused(false); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::StepOut(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| DCHECK(connector_->server()); |
| connector_->script_debugger()->StepOut(); |
| connector_->server()->SetPaused(false); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::StepOver(const JSONObject& params) { |
| UNREFERENCED_PARAMETER(params); |
| DCHECK(connector_->script_debugger()); |
| DCHECK(connector_->server()); |
| connector_->script_debugger()->StepOver(); |
| connector_->server()->SetPaused(false); |
| return JSONObject(new base::DictionaryValue()); |
| } |
| |
| void JavaScriptDebuggerComponent::OnScriptDebuggerDetach( |
| const std::string& reason) { |
| DLOG(INFO) << "JavaScript debugger detached: " << reason; |
| } |
| |
| void JavaScriptDebuggerComponent::OnScriptDebuggerPause( |
| scoped_ptr<script::CallFrame> call_frame) { |
| // Notify the clients we're about to pause. |
| SendPausedEvent(call_frame.Pass()); |
| |
| // Tell the debug server to enter paused state - block this thread. |
| DCHECK(connector_->server()); |
| connector_->server()->SetPaused(true); |
| |
| // Notify the clients we've resumed. |
| SendResumedEvent(); |
| } |
| |
| void JavaScriptDebuggerComponent::OnScriptFailedToParse( |
| scoped_ptr<script::SourceProvider> source_provider) { |
| DCHECK(source_provider); |
| HandleScriptEvent(kScriptFailedToParse, source_provider.Pass()); |
| } |
| |
| void JavaScriptDebuggerComponent::OnScriptParsed( |
| scoped_ptr<script::SourceProvider> source_provider) { |
| DCHECK(source_provider); |
| HandleScriptEvent(kScriptParsed, source_provider.Pass()); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::CreateCallFrameData( |
| const scoped_ptr<script::CallFrame>& call_frame) { |
| DCHECK(call_frame); |
| |
| // Create the JSON object and add the data for this call frame. |
| JSONObject call_frame_data(new base::DictionaryValue()); |
| call_frame_data->SetString(kCallFrameId, call_frame->GetCallFrameId()); |
| call_frame_data->SetString(kFunctionName, call_frame->GetFunctionName()); |
| |
| // Offset the line number according to the start line of the source. |
| const std::string script_id = call_frame->GetScriptId(); |
| int line_number = call_frame->GetLineNumber(); |
| DCHECK(source_providers_[script_id]); |
| base::optional<int> start_line = source_providers_[script_id]->GetStartLine(); |
| line_number -= start_line.value_or(1); |
| |
| // Add the location data. |
| call_frame_data->SetString(kLocationScriptId, script_id); |
| call_frame_data->SetInteger(kLocationLineNumber, line_number); |
| base::optional<int> column_number = call_frame->GetColumnNumber(); |
| if (column_number) { |
| call_frame_data->SetInteger(kLocationColumnNumber, column_number.value()); |
| } |
| |
| // Add the scope chain data. |
| JSONList scope_chain_data(CreateScopeChainData(call_frame->GetScopeChain())); |
| call_frame_data->Set(kScopeChain, scope_chain_data.release()); |
| |
| // Add the "this" object data. |
| const script::OpaqueHandleHolder* this_object = call_frame->GetThis(); |
| if (this_object) { |
| JSONObject this_object_data(connector_->CreateRemoteObject(this_object)); |
| call_frame_data->Set(kThis, this_object_data.release()); |
| } |
| |
| return call_frame_data.Pass(); |
| } |
| |
| JSONList JavaScriptDebuggerComponent::CreateCallStackData( |
| scoped_ptr<script::CallFrame> call_frame) { |
| JSONList call_frame_list(new base::ListValue()); |
| |
| // Consume the scoped CallFrame objects as we iterate over them. |
| while (call_frame) { |
| JSONObject call_frame_data(CreateCallFrameData(call_frame)); |
| DCHECK(call_frame_data); |
| call_frame_list->Append(call_frame_data.release()); |
| scoped_ptr<script::CallFrame> next_call_frame(call_frame->GetCaller()); |
| call_frame = next_call_frame.Pass(); |
| } |
| |
| return call_frame_list.Pass(); |
| } |
| |
| JSONObject JavaScriptDebuggerComponent::CreateScopeData( |
| const scoped_ptr<script::Scope>& scope) { |
| DCHECK(scope); |
| const script::OpaqueHandleHolder* scope_object = scope->GetObject(); |
| JSONObject scope_data(new base::DictionaryValue()); |
| scope_data->Set(kObject, |
| connector_->CreateRemoteObject(scope_object).release()); |
| scope_data->SetString(kType, script::Scope::TypeToString(scope->GetType())); |
| return scope_data.Pass(); |
| } |
| |
| JSONList JavaScriptDebuggerComponent::CreateScopeChainData( |
| scoped_ptr<script::Scope> scope) { |
| JSONList scope_chain_list(new base::ListValue()); |
| |
| // Consume the scoped Scope objects as we iterate over them. |
| while (scope) { |
| JSONObject scope_data(CreateScopeData(scope)); |
| DCHECK(scope_data); |
| scope_chain_list->Append(scope_data.release()); |
| scoped_ptr<script::Scope> next_scope(scope->GetNext()); |
| scope = next_scope.Pass(); |
| } |
| |
| return scope_chain_list.Pass(); |
| } |
| |
| void JavaScriptDebuggerComponent::HandleScriptEvent( |
| const std::string& method, |
| scoped_ptr<script::SourceProvider> source_provider) { |
| DCHECK(source_provider); |
| |
| // Send the event notification to the debugger clients. |
| JSONObject params(new base::DictionaryValue()); |
| params->SetString(kScriptId, source_provider->GetScriptId()); |
| params->SetString(kUrl, source_provider->GetUrl()); |
| base::optional<int> error_line = source_provider->GetErrorLine(); |
| if (error_line) { |
| DCHECK_EQ(method, kScriptFailedToParse); |
| params->SetInteger(kErrorLine, error_line.value()); |
| } |
| base::optional<std::string> error_message = |
| source_provider->GetErrorMessage(); |
| if (error_message) { |
| DCHECK_EQ(method, kScriptFailedToParse); |
| params->SetString(kErrorMessage, error_message.value()); |
| } |
| connector_->SendEvent(method, params); |
| |
| // Store the raw pointer to the source provider in the map. |
| // The values in the map will be deleted on destruction by |
| // |source_providers_deleter_|. |
| const std::string script_id = source_provider->GetScriptId(); |
| SourceProviderMap::iterator it = source_providers_.find(script_id); |
| if (it != source_providers_.end()) { |
| delete it->second; |
| } |
| source_providers_[script_id] = source_provider.release(); |
| } |
| |
| void JavaScriptDebuggerComponent::ResolveBreakpoint( |
| const Breakpoint& breakpoint, std::vector<ScriptLocation>* locations) { |
| for (SourceProviderMap::iterator it = source_providers_.begin(); |
| it != source_providers_.end(); ++it) { |
| script::SourceProvider* script = it->second; |
| if (script->GetUrl() == breakpoint.url) { |
| connector_->script_debugger()->SetBreakpoint( |
| script->GetScriptId(), |
| breakpoint.line_number + script->GetStartLine().value_or(1), |
| breakpoint.column_number); |
| locations->push_back(ScriptLocation(script->GetScriptId(), |
| breakpoint.line_number, |
| breakpoint.column_number)); |
| } |
| } |
| } |
| |
| void JavaScriptDebuggerComponent::SendPausedEvent( |
| scoped_ptr<script::CallFrame> call_frame) { |
| std::string event_method = kPaused; |
| JSONObject event_params(new base::DictionaryValue()); |
| JSONList call_stack_data(CreateCallStackData(call_frame.Pass())); |
| DCHECK(call_stack_data); |
| event_params->Set(kCallFrames, call_stack_data.release()); |
| event_params->SetString(kReason, kDebugCommand); |
| connector_->SendEvent(event_method, event_params); |
| } |
| |
| void JavaScriptDebuggerComponent::SendResumedEvent() { |
| // Send the event to the clients. No parameters. |
| std::string event_method = kResumed; |
| scoped_ptr<base::DictionaryValue> event_params(new base::DictionaryValue()); |
| connector_->SendEvent(event_method, event_params); |
| } |
| |
| } // namespace debug |
| } // namespace cobalt |