揭秘浏览器远程调试技术 | Taobao FED | 淘宝前端团队

·  阅读 3686
揭秘浏览器远程调试技术 | Taobao FED | 淘宝前端团队
原文链接: taobaofed.org

调试技术的起源

1947 年 9 月 9 日,一名美国的科学家格蕾丝.霍普和她的同伴在对 Mark II 计算机进行研究的时候发现,一只飞蛾粘在一个继电器上,导致计算机无法正常工作,当他们把飞蛾移除之后,计算机又恢复了正常运转。于是他们将这只飞蛾贴在了他们当时记录的日志上,对这件事情进行了详细的记录,并在日志最后写了这样一句话:First actual case of bug being found。这是他们发现的第一个真正意义上的 bug,这也是人类计算机软件历史上,发现的第一个 bug,而他们找到飞蛾的方法和过程,就是 debugging 调试技术。

History of Debug

从格蕾丝调试第一个 bug 到现在,69 年的时间里,在计算机领域,硬件、软件各种调试技术都在不断的发展和演进。那么对于日新月异的前端来说,调试技术也尤其显得重要。淘宝前端团队也正在使用一些创新的技术和手段来解决无线页面调试的问题。今天先跟大家分享下浏览器远程调试技术,本文将用 Chrome/Webview 来作为案例。

调试原理

调试方式与权限管理

-

目前常规浏览器调试目标分为两种:Chrome PC 浏览器和 Chrome Mobile(Android 4.4 以后,Android WebView 其实就是 Chromium WebView)。

Chrome PC 浏览器

对于调试 Chrome PC 浏览器,可能大家经常使用的是用鼠标右键或者快捷方式(mac:option + command + J),唤起 Chrome 的控制台,来对当前页面进行调试。其实还有另外一种方法,就是使用一个 Chrome 浏览器调试另一个 Chrome 浏览器。Chrome 启动的时候,默认是关闭了调试端口的,如果要对一个目标 Chrome PC 浏览器进行调试,那么启动的时候,可以通过传递参数来开启 Chrome 的调试开关:

# for mac
sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

Chrome Android 浏览器

对于调试 Android 上的 Chrome 或者 WebView 需要连接 USB 线。打开调试端口的方法如下:

adb forward tcp:9222 localabstract:chrome_devtools_remote

跟 Chrome PC 浏览器不同的是,对于 Chrome Android 浏览器,由于数据传输是通过 USB 线而不是 WIFI,实际上 Chrome Android 创建的一个 chrome_devtools_remote 这个 path 的 domain socket。所以,上面一条命令则是通过 Android 的 adb 将 PC 的端口 9222 通过 USB 线与 chrome_devtools_remote 这个 domain socket 建立了一个端口映射。

权限管理

Google 为了限制调试端口的接入范围,对于 Chrome PC 浏览器,调试端口只接受来自 127.0.0.1 或者 localhost 的数据请求,所以,你无法通过你的本地机器 IP 来调试 Chrome。对于 Android Chrome/WebView,调试端口只接受来自于 shell 这个用户数据请求,也就是说只能通过 USB 进行调试,而不能通过 WIFI。

开始调试

通过以上的调试方式的接入以及调试端口的打开,这个时候在浏览器中输入:

http://127.0.0.1:9222/json

将会看到类似下面的内容:

[
  {
    "description": "",
    "devtoolsFrontendUrl": "/devtools/inspector.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e",
    "id": "ebdace60-d482-4340-b622-a6198e7aad6e",
    "title": "揭秘浏览器远程调试技术.mdown—/Users/harlen/Documents",
    "type": "page",
    "url": "http://127.0.0.1:51004/view/61",
    "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e"
  }
  ]

其中,最重要的 2 个参数分别是 id 和 webSocketDebuggerUrl。Chrome 会为每个页面分配一个唯一的 id,作为该页面的唯一标识符。几乎对目标浏览器的所有操作都是需要带上这个 id。

Chrome 提供了以下这些 http 接口控制目标浏览器

# 获取当前所有可调式页面信息
http://127.0.0.1:9222/json
# 获取调试目标 WebView/blink 的版本号
http://127.0.0.1:9222/json/version
# 创建新的 tab,并加载 url
http://127.0.0.1:9222/json/new?url
# 关闭 id 对应的 tab
http://127.0.0.1:9222/json/close/id

webSocketDebuggerUrl 则在调试该页面需要用到的一个 WebSocket 连接。chrome 的 devtool 的所有调试功能,都是基于 Remote Debugging Protocol 使用 WebSocket 来进行数据传输的。那么这个 WebSocket,就是上面我们从 http://127.0.0.1:9222/json 获取的 webSocketDebuggerUrl,每一个页面都有自己不同的 webSocketDebuggerUrl。这个 webSocketDebuggerUrl是通过 url 的 query 参数传递给 chrome devtool 的。

chrome 的 devtool 可以从 Chrome 浏览器中进行提取 devtool 源码或者从 blink 源码中获取。在部署好自己的 chrome devtool 代码之后,下面既可以开始对 Chrome 进行调试, 浏览器输入一下内容:

http://path_to_your_devtool/devtool.html?ws=127.0.0.1:9222/devtools/page/ebdace60-d482-4340-b622-a6198e7aad6e

其中 ws 这个参数的值就是上面出现的 webSocketDebuggerUrl。Chrome 的 devtool 会使用这个 url 创建 WebSocket 对该页面进行调试。

如何实现 JavaScript 调试

在进入 Chrome 的 devtool 之后,我们可以调出控制台,来查看 devtool 的 WebSocket 数据。这个里面有很多数据,我这里只讲跟 JavaScript 调试相关的。 -

图中,对于 JavaScript 调试,有一条非常重要的消息,我蓝色选中的那条消息:

{"id":6,"method":"Debugger.enable"}

然后选中要调试的 JavaScript 文件,然后设置一个断点,我们再来看看 WebSocket 消息: -

devtool 像目标 Chrome 发送了 2 条消息

{
  "id": 23,
  "method": "Debugger.getScriptSource",
  "params": {
    "scriptId": "103"
  }
  }

{
  "id": 24,
  "method": "Debugger.setBreakpointByUrl",
  "params": {
    "lineNumber": 2,
    "url": "https://g.alicdn.com/alilog/wlog/0.2.10/??aplus_wap.js,spm_wap.js,spmact_wap.js",
    "columnNumber": 0,
    "condition": ""
  }
  }

那么收到这几条消息之后,V8 做了些什么呢? 我们先来简单的看下 V8 里面的一小段源码片段:

// V8 Debugger.cpp
DispatcherImpl(FrontendChannel* frontendChannel, Backend* backend) : DispatcherBase(frontendChannel), m_backend(backend) {
    m_dispatchMap["Debugger.enable"] = &DispatcherImpl::enable;
    m_dispatchMap["Debugger.disable"] = &DispatcherImpl::disable;
    m_dispatchMap["Debugger.setBreakpointsActive"] = &DispatcherImpl::setBreakpointsActive;
    m_dispatchMap["Debugger.setSkipAllPauses"] = &DispatcherImpl::setSkipAllPauses;
    m_dispatchMap["Debugger.setBreakpointByUrl"] = &DispatcherImpl::setBreakpointByUrl;
    m_dispatchMap["Debugger.setBreakpoint"] = &DispatcherImpl::setBreakpoint;
    m_dispatchMap["Debugger.removeBreakpoint"] = &DispatcherImpl::removeBreakpoint;
    m_dispatchMap["Debugger.continueToLocation"] = &DispatcherImpl::continueToLocation;
    m_dispatchMap["Debugger.stepOver"] = &DispatcherImpl::stepOver;
    m_dispatchMap["Debugger.stepInto"] = &DispatcherImpl::stepInto;
    m_dispatchMap["Debugger.stepOut"] = &DispatcherImpl::stepOut;
    m_dispatchMap["Debugger.pause"] = &DispatcherImpl::pause;
    m_dispatchMap["Debugger.resume"] = &DispatcherImpl::resume;
    m_dispatchMap["Debugger.searchInContent"] = &DispatcherImpl::searchInContent;
    m_dispatchMap["Debugger.setScriptSource"] = &DispatcherImpl::setScriptSource;
    m_dispatchMap["Debugger.restartFrame"] = &DispatcherImpl::restartFrame;
    m_dispatchMap["Debugger.getScriptSource"] = &DispatcherImpl::getScriptSource;
    m_dispatchMap["Debugger.setPauseOnExceptions"] = &DispatcherImpl::setPauseOnExceptions;
    m_dispatchMap["Debugger.evaluateOnCallFrame"] = &DispatcherImpl::evaluateOnCallFrame;
    m_dispatchMap["Debugger.setVariableValue"] = &DispatcherImpl::setVariableValue;
    m_dispatchMap["Debugger.setAsyncCallStackDepth"] = &DispatcherImpl::setAsyncCallStackDepth;
    m_dispatchMap["Debugger.setBlackboxPatterns"] = &DispatcherImpl::setBlackboxPatterns;
    m_dispatchMap["Debugger.setBlackboxedRanges"] = &DispatcherImpl::setBlackboxedRanges;
    }

你会发现,V8 有 m_dispatchMap 这样一个 Map。专门用来处理所有 JavaScript 调试相关的处理。 其中就有本文即将重点讲述的:

  • Debuggger.enable
  • Debugger.getScriptSource
  • setBreakpointByUrl

这些都需要在 V8 的源码中找到答案。顺便给大家推荐一个查看 Chromium/V8 最正确的方式是使用 cs.chromium.org,比 SourceInsight 还要方便。

Debugger.enable

void V8Debugger::enable() {
    if (m_enableCount++) return;
    DCHECK(!enabled());
    v8::HandleScope scope(m_isolate);
    v8::Debug::SetDebugEventListener(m_isolate, &V8Debugger::v8DebugEventCallback,
    v8::External::New(m_isolate, this));
    m_debuggerContext.Reset(m_isolate, v8::Debug::GetDebugContext(m_isolate));
    compileDebuggerScript();
    }

这个接口的名称叫 Debugger.enable,但是收到这条消息,V8 其实就干了两件事情事情:

  • SetDebugEventListener: 给 JavaScript 调试安装监听器,并设置 v8DebugEventCallback 这个回调函数。JavaScript 所有的调试事件,都会被这个监听器捕获,包括:JavaScript 异常停止,断点停止,单步调试等等。

  • compileDebuggerScript: 编译 V8 内置的 JavaScript 文件 debugger-script.js。由于这文件比较长,我这里就不贴出来了,感兴趣的同学点击这个链接进行查看源码。debugger-script.js 主要是定义了一些针对 JavaScript 断点进行操作的函数,例如设置断点、查找断点以及单步调试相关的函数。那么这个 debugger-script.js 文件,被 V8 进行编译之后,保存在 global 对象上,等待对 JavaScript 进行调试的时候,被调用。

    Debugger.getScriptSource

    在 Chrome 解析引擎解析到

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改