远程调试协议

Mozilla 调试协议允许调试器连接到浏览器,发现可以调试或检查的内容,选择要监视的 JavaScript 线程,以及观察和修改其执行情况。该协议提供了 JavaScript、DOM 节点、CSS 规则以及客户端 Web 应用程序中使用的其他技术的统一视图。该协议应该足够通用,以便扩展到与其他类型的客户端(例如分析器)和服务器(邮件阅读器;随机 XULrunner 应用程序)一起使用。

调试器(客户端)和浏览器(服务器)之间所有通信都采用 JSON 对象的形式。这使得协议可以直接被人阅读,能够优雅地演变,并且易于使用现成的库来实现。特别是,创建用于测试和实验的模拟实现应该很容易。

该协议在 JavaScript 级别运行,而不是在 C++ 或机器级别运行,并假设 JavaScript 实现本身是健康且响应迅速的。正在执行的 JavaScript 程序可能出现了错误,但 JavaScript 实现的内部状态不能损坏。实现中的错误可能会导致调试器失败;解释程序中的错误则不会。

通用约定

Actor

Actor 是服务器上可以与客户端交换 JSON 数据包的东西。客户端发出的每个数据包都指定了其目标 Actor,服务器发出的每个数据包都指示了发送它的 Actor。

每个服务器都有一个根 Actor,客户端首先与之交互。根 Actor 可以解释服务器代表什么类型的东西(浏览器;邮件阅读器;等等),并枚举可供调试的内容:选项卡、chrome 等等。这些内容中的每一个又都由一个可以向其发送请求的 Actor 表示。被调试程序的工件(如 JavaScript 对象和堆栈帧)和调试机制的工件(如断点和观察点)都是可以与其交换数据包的 Actor。

例如,调试器可能连接到浏览器,请求根 Actor 列出浏览器的选项卡,并将此列表呈现给开发人员。如果开发人员选择一些选项卡进行调试,则调试器可以向表示这些选项卡的 Actor 发送 attach 请求,以开始调试。

Actor 名称是 JSON 字符串,不包含空格或冒号。根 Actor 的名称为 "root"

为了允许服务器重用 Actor 名称及其所需资源,Actor 的生命周期有限。服务器中的所有 Actor 形成一棵树,其根是根 Actor。关闭与 Actor 的通信会自动关闭与其子级的通信。例如,表示线程堆栈帧的 Actor 是表示线程本身的 Actor 的子级,因此当调试器从线程分离时(关闭线程的 Actor),帧的 Actor 会自动关闭。这种安排允许协议自由地提及 Actor,而无需让客户端负责显式关闭曾经提及的每个 Actor。

当我们说某个 Actor A 是某个 Actor B 的子级时,我们的意思是 AB 的直接子级,而不是孙子、曾孙子等等。类似地,父级表示“直接父级”。我们使用术语祖先后代来指代那些较松散的关系。

根 Actor 没有父级,并且只要底层与客户端的连接存在,它就会一直存在;当该连接关闭时,所有 Actor 都将关闭。

请注意,Actor 层次结构通常不对应于被调试程序中出现的任何特定层次结构。例如,尽管 Web Worker 以层次结构排列,但表示 Web Worker 线程的 Actor 都是根 Actor 的子级:人们可能希望从父 Worker 分离,同时继续调试其子级之一,因此关闭子 Worker 的通信仅仅因为关闭了其父级的通信是没有意义的。

(我们从 Mozilla 的 IPDL 中“借用”了“Actor”这个术语,大致意思是“参与协议的事物”。但是,IPDL 对这个概念做了更多的事情:它将客户端和服务器都视为 Actor 集合,并使用该细节来静态验证协议的属性。相反,调试协议只是希望有一种一致的方法来指示数据包的目标实体。)

数据包

该协议由可靠的双向字节流承载;两个方向发送的数据都由称为数据包的 JSON 对象组成。数据包是顶级 JSON 对象,不包含在任何其他值中。

客户端发送的每个数据包都具有以下形式

{ "to":actor, "type":type, ... }

其中 actor 是数据包目标 Actor 的名称,type 是一个字符串,指定了数据包的类型。根据 type,可能存在其他属性。

服务器发送的每个数据包都具有以下形式

{ "from":actor, ... }

其中 actor 是发送该数据包的 Actor 的名称。根据情况,数据包可能具有其他属性。

如果数据包的目标是已不存在的 Actor,则服务器会向客户端发送以下形式的数据包

{ "from":actor, "error":"noSuchActor" }

其中 actor 是不存在的 Actor 的名称。(从不存在的 Actor 接收消息很奇怪,但客户端显然认为该 Actor 存在,此回复允许客户端将错误报告与问题来源配对。)

客户端应该静默忽略它们无法识别的包属性。我们预计,随着协议的演变,我们将指定可以出现在现有数据包中的新属性,实验性实现也将这样做。

Actor 通信的常见模式

每种类型的 Actor 都指定了它可以接收哪些数据包、它可能发送哪些数据包以及何时可以执行每个操作。虽然原则上这些交互规则可能很复杂,但在实践中大多数 Actor 都遵循两种简单模式之一

  • 请求/回复:发送到 Actor 的每个数据包(“请求”)都会引发一个响应数据包(“回复”)。

  • 请求/回复/通知:类似于请求/回复,但 Actor 可能会发送不是对任何特定请求的响应的数据包(“通知”),可能宣布在被调试程序中自发发生的事件。

这些模式将在下面详细介绍。

某些 Actor 需要更复杂的规则。例如,类线程 Actor 接受的数据包集取决于它所处的四种状态中的哪一种。Actor 可能会自发地从一种状态转换到另一种状态,并且并非所有状态转换都会生成通知数据包。像这样的 Actor 需要仔细指定。

请求/回复模式

在本规范中,如果我们称某个数据包为请求,那么它是由客户端发送的数据包,它总是会引发 Actor 返回的单个数据包,即回复。这些术语表示一种简单的通信模式:Actor 按接收到的顺序处理数据包,客户端可以相信第 i 个回复对应于第 i 个请求。

来自请求/回复 Actor 的错误回复数据包构成一个回复。

请注意,客户端可以向请求/回复 Actor 发送多个请求,而无需等待每个请求的回复,然后再发送下一个请求;请求可以流水线化。但是,由于挂起的请求会消耗内存,因此客户端应确保在任何时间都只有有限数量的请求处于挂起状态。

请求/回复/通知模式

某些 Actor 遵循请求/回复模式,但还可能向客户端发送通知数据包,而不是对任何特定请求的回复。例如,如果客户端向根 Actor 发送 ["listTabs"](#listing-browser-tabs) 请求,则根 Actor 会发送回复。但是,由于客户端现在已表达了对打开选项卡列表的兴趣,因此根 Actor 可能会随后向客户端发送 "tabListChanged" 通知数据包,指示如果客户端对最新状态感兴趣,则应重新获取选项卡列表。

任何 Actor 在从客户端接收到的数据包之间发送的通知数据包数量应该有一个较小的上限,以确保 Actor 不会淹没客户端。在上面的示例中,根 Actor 在每个 "listTabs" 请求之后最多发送一个 "tabListChanged" 通知。

错误数据包

任何 Actor 都可以使用以下形式的错误回复来回复它无法处理的数据包

{ "from":actor, "error":name, "message":message }

其中 name 是一个 JSON 字符串,命名了出错的原因,message 是一个英文错误消息。错误 name 由协议指定;客户端可以使用该名称来识别出现了哪种错误情况。message 在不同的实现中可能会有所不同,并且只应作为最后手段显示给用户,因为服务器缺乏足够的信息来提供适当的消息。

如果 Actor 接收到的数据包类型它无法识别,它会发送以下形式的错误回复

{ "from":actor, "error":"unrecognizedPacketType", "message":message }

其中 message 提供了帮助调试器开发人员了解出错原因的详细信息:Actor 是什么类型的 Actor;接收到的数据包;等等。

如果 Actor 接收到的数据包缺少必要的参数(例如,没有 "text" 参数的 "autocomplete" 数据包),它会发送以下形式的错误回复

{ "from":actor, "error":"missingParameter", "message":message }

其中 message 提供了帮助调试器开发人员修复问题的详细信息。

如果 Actor 接收到的数据包的参数值不适合该操作,它会发送以下形式的错误回复

{ "from":actor, "error":"badParameterType", "message":message }

其中 message 提供了帮助调试器开发人员修复问题的详细信息。(某些数据包的描述为特定情况指定了更具体的错误。)

Grip

Grip 是一个 JSON 值,它引用被调试程序中的特定 JavaScript 值。Grip 出现在需要将被调试程序中的任意值传达给客户端的任何地方:堆栈帧、对象属性列表、词法环境、paused 数据包等等。

对于可变值(如对象和数组),抓取器不仅仅将值的当前状态传达给客户端。它们还充当原始值的引用,通过包含一个参与者,客户端可以向其发送消息以修改调试目标中的值。

抓取器具有以下其中一种形式

value

其中 value 是字符串、数字或布尔值。对于这些类型的值,抓取器只是该值的 JSON 形式。

{ "type":"null" }

这表示 JavaScript null 值。(为了方便用 JavaScript 实现的客户端,协议并非简单地用 JSON null 来表示 JavaScript null:这种表示允许此类客户端使用 typeof(grip) == "object" 来判断抓取器是否简单。)

{ "type":"undefined" }

这表示 JavaScript undefined 值。(undefined 在 JSON 中没有直接表示。)

{ "type":"Infinity" }

这表示 JavaScript Infinity 值。(Infinity 在 JSON 中没有直接表示。)

{ "type":"-Infinity" }

这表示 JavaScript -Infinity 值。(-Infinity 在 JSON 中没有直接表示。)

{ "type":"NaN" }

这表示 JavaScript NaN 值。(NaN 在 JSON 中没有直接表示。)

{ "type":"-0" }

这表示 JavaScript -0 值。(-0 转换为 JSON 字符串为 0。)

{ "type":"object", "class":className, "actor":actor }

这表示一个 JavaScript 对象,其类为 className。(为了形成抓取器,数组和函数被视为对象。)可以查询 Actor 以获取对象的內容,如下所述。

如果类为“Function”,则抓取器可能具有其他属性

{ "type":"object", "class":"Function", "actor":actor,
  "name":name, "displayName":displayName,
  "userDisplayName":userDisplayName,
  "url":url, "line":line, "column":column }

这些附加属性是

名称

函数的名称(如源代码中 function 关键字之后所示),以字符串形式表示。如果函数是匿名的,则省略 name 属性。

displayName

系统推断出的函数名称(例如,"Foo.method")。如果函数具有指定的名称(在抓取器中作为 "name" 属性出现),或者系统无法为其推断出合适的名称,则省略 displayName 属性。

userDisplayName

如果函数对象具有一个 "displayName" 值属性,其值为字符串,则此属性的值为该属性的值。(许多 JavaScript 开发工具会查询此类属性,以便为开发人员提供一种为函数提供他们自己有意义的名称的方法。)

url

函数源位置的 URL(请参阅源位置);

line

函数源位置的行号(请参阅源位置);

column

函数源位置的列号(请参阅源位置);

{ "type":"longString", "initial":initial, "length":length, "actor":actor }

这表示一个非常长的字符串,其中“非常长”由服务器自行决定。Initial 是字符串的某个初始部分,length 是字符串的完整长度,并且可以查询 Actor 以获取字符串的其余部分,如下所述。

例如,下表显示了一些 JavaScript 表达式以及将在协议中表示它们的抓取器

JavaScript 表达式 抓取器
42 42
true true
"nasu" "nasu"
(void 0) { "type":"undefined" }
({x:1}) { "type":"object", "class":"Object", "actor":"24" }
"Arms and the man I sing, who, [much, much more text]" { "type":"longString", "initial":"Arms and the man I sing", "length":606647, "actor":"25" }

垃圾回收永远不会释放通过协议对客户端可见的对象。因此,表示 JavaScript 对象的 Actor 实际上是垃圾回收根。

对象

当线程暂停时,客户端可以向对象抓取器中出现的 Actor 发送请求,以更详细地检查它们表示的对象。

属性描述符

向客户端描述对象的属性的协议请求通常使用**描述符**(以 ECMAScript 5 的属性描述符为模型的 JSON 值)来描述各个属性。

描述符具有以下形式

{ "enumerable":<enumerable>, "configurable":<configurable>, ... }

其中enumerableconfigurable是布尔值,指示属性是否可枚举和可配置,并且根据属性的类型存在其他属性。

数据属性的描述符具有以下形式

{ "enumerable":<enumerable>, "configurable":<configurable>,
  "value":<value>, "writeable":<writeable> }

其中value是对属性值的抓取,writeable是布尔值,指示属性是否可写。

访问器属性的描述符具有以下形式

{ "enumerable":<enumerable>, "configurable":<configurable>,
  "get":<getter>, "set":<setter> }

其中gettersetter是对属性的 getter 和 setter 函数的抓取。如果属性缺少给定的访问器函数,则这些函数可能是{ "type":"undefined" }

**安全 getter 值描述符**提供一个值,该值是在应用于实例时继承的访问器返回的值。(请参阅查找对象的原型和属性,了解为什么以及何时使用此类描述符。)此类描述符具有以下形式

{ "getterValue": <value>, "getterPrototypeLevel": <level>,
  "enumerable":<enumerable>, "writable":<writable> }

其中value是对 getter 返回的值的抓取,level是在对象原型链上向上查找 getter 作为自身属性出现在的对象所需执行的步骤数。如果 getter 直接出现在对象上,则level为零。如果继承的访问器具有 setter,则writable属性为 true,否则为 false。

例如,如果正在调试的 JavaScript 程序评估表达式

({x:10, y:"kaiju", get a() { return 42; }})

则此值的抓取将具有以下形式

{ "type":"object", "class":"Object", "actor":<actor> }

并且向actor发送“prototypeAndProperties”请求将产生以下回复

{ "from":<actor>, "prototype":{ "type":"object", "class":"Object", "actor":<objprotoActor> },
  "ownProperties":{ "x":{ "enumerable":true, "configurable":true, "writeable":true, "value":10 },
                    "y":{ "enumerable":true, "configurable":true, "writeable":true, "value":"kaiju" },
                    "a":{ "enumerable":true, "configurable":true,
                          "get":{ "type":"object", "class":"Function", "actor":<getterActor> },
                          "set":{ "type":"undefined" }
                        }
                   }
}

向引用 DOM 鼠标事件的对象 Actor 发送“prototypeAndProperties”请求可能会产生以下回复

{ "from":<mouseEventActor>, "prototype":{ "type":"object", "class":"MouseEvent", "actor":<mouseEventProtoActor> },
  "ownProperties":{ }
  "safeGetterValues":{ "screenX": { "getterValue": 1000, "getterPrototypeLevel": 1,
                                    "enumerable": true, "writable": false },
                       "screenY": { "getterValue": 1000, "getterPrototypeLevel": 1,
                                    "enumerable": true, "writable": false },
                       "clientX": { "getterValue": 800,  "getterPrototypeLevel": 1,
                                    "enumerable": true, "writable": false },
                       "clientY": { "getterValue": 800,  "getterPrototypeLevel": 1,
                                    "enumerable": true, "writable": false },
                       ...
                     }
}
查找对象的原型和属性

要检查对象的原型和属性,客户端可以向对象的抓取器的 Actor 发送以下形式的请求

{ "to":<gripActor>, "type":"prototypeAndProperties" }

抓取器 Actor 将回复

{ "from":<gripActor>, "prototype":<prototype>, "ownProperties":<ownProperties> }

其中prototype是对对象原型的抓取(可能是{ "type":"null" }),并且ownProperties具有以下形式

{ name:<descriptor>, ... }

带有name对象每个自身属性的配对。

Web 广泛使用继承的访问器属性;例如,鼠标单击事件的clientXclientY>属性实际上是访问器属性,事件对象从其原型链继承这些属性。如果服务器可以确定可以无副作用地调用 getter,则直接在对象上显示此类属性的值(注意将其与真正的“自身”属性区分开来)可能非常有价值。

为此,在可能的情况下,服务器可能会为对象提供安全 getter 值描述符,如上文属性描述符中所述,报告在应用于该对象时在对象原型链上找到的 getter 函数返回的值。如果服务器选择提供任何值,则回复将包含一个"safeGetterValues"属性,其形式为

{ name:<descriptor>, ... }

带有name对象从其原型链继承的每个安全 getter 或直接出现在对象上的每个安全 getter 的配对。此处的每个descriptor都是一个安全 getter 值描述符。

待办事项:具有许多属性的对象怎么办?

查找对象的原型

要查找对象的原型,客户端可以向对象的抓取器的 Actor 发送以下形式的请求

{ "to":<gripActor>, "type":"prototype" }

抓取器 Actor 将回复

{ "from":<gripActor>, "prototype":<prototype> }

其中prototype是对对象原型的抓取(可能是{ "type":"null" })。

列出对象自身属性的名称

要列出对象自身属性的名称,客户端可以向对象的抓取器的 Actor 发送以下形式的请求

{ "to":<gripActor>, "type":"ownPropertyNames" }

抓取器 Actor 将回复

{ "from":<gripActor>, "ownPropertyNames":[ <name>, ... ] }

其中每个name都是一个字符串,命名对象的自身属性。

查找单个属性的描述符

要获取对象特定属性的描述符,客户端可以向对象的抓取器的 Actor 发送以下形式的请求

{ "to":<gripActor>, "type":"property", "name":<name> }

抓取器 Actor 将回复

{ "from":<gripActor>, "descriptor":<descriptor> }

其中descriptor是命名为name的对象自身属性的描述符,如果对象没有此类自身属性,则为null

属性描述符具有以下形式

{ "configurable":<configurable>, "enumerable":<enumerable>, ... }

其中configurableenumerable是布尔值。如果可以删除属性或更改其属性,则Configurable为 true。如果属性将由for-in枚举枚举,则Enumerable为 true。

值属性的描述符具有以下形式

{ "configurable":<configurable>, "enumerable":<enumerable>,
    "writable":<writable>, "value":<value> }

其中writabletrue,如果可以写入属性的值;value是对属性值的抓取;configurableenumerable如上所述。

访问器属性的描述符具有以下形式

{ "configurable":<configurable>, "enumerable":<enumerable>,
    "get":<get>, "set":<set> }

其中getset是对属性的 getter 和 setter 函数的抓取;如果属性缺少给定的访问器函数,则省略其中一个或两个。Configurableenumerable如上所述。

待办事项:分配给值属性

待办事项:数组的特殊内容

待办事项:函数的特殊内容

待办事项:查找函数的源位置

待办事项:按顺序获取函数的命名参数

待办事项:Harmony 代理的描述符

函数

如果抓取器中给出的对象的类为"Function",则抓取器的 Actor 将响应此处给出的消息。

{ "to":<functionGripActor>, "type":"parameterNames" }

这请求由functionGripActor表示的函数的参数名称。回复具有以下形式

{ "from":<functionGripActor>, "parameterNames":[ <parameter>, ... ] }

其中每个parameter都是函数的形式参数的名称,以字符串形式表示。如果函数采用解构参数,则parameter是 JSON 数组和对象形式的结构,与解构参数的形式匹配。

{ "to":<functionGripActor>, "type":"scope" }

返回函数已关闭的词法环境。回复具有以下形式

{ "from":<functionGripActor>, "scope":<environment> }

其中environment词法环境。请注意,服务器仅返回正在调试的上下文中函数的环境;如果函数的全局范围不是我们附加到的浏览上下文,则函数抓取器 Actor 将发送以下形式的错误回复

{ "from":<functionGripActor>, "error":"notDebuggee", "message":<message> }

其中message是解释问题的文本。

{ "to":<functionGripActor>, "type":"decompile", "pretty":<pretty> }

返回与由functionGripActor表示的函数等效的 JavaScript 源代码。如果可选的pretty参数存在且prettytrue,则生成带换行符的缩进源代码。回复具有以下形式

{ "from":<functionGripActor>, "decompiledCode":<code> }

其中code是字符串。

如果functionGripActor的引用不是函数,或者是一个函数代理,则 Actor 将使用以下形式的错误回复来响应这些请求

{ "from":<functionGripActor>, "error":"objectNotFunction", message:<message> }

其中message是包含任何其他信息(对调试器开发人员有帮助)的字符串。

长字符串

客户端可以通过向长字符串抓取器 Actor 发送以下形式的请求来查找长字符串的完整内容

{ "to":<gripActor>, "type":"substring", "start":<start>, "end":<end> }

其中startend是整数。这请求从第start个字符开始,在第end个字符之前结束的子字符串。Actor 将回复如下

{ "from":<gripActor>, "substring":<string> }

其中string是 Actor 表示的字符串的请求部分。小于零的start值将被视为零;大于字符串长度的值将被视为字符串的长度。end的值也类似地处理。如果end小于start,则交换这两个值。(这旨在与 JavaScript 的String.prototype.substring的行为相同。)

与任何其他 Actor 一样,客户端只能在长字符串抓取器 Actor 生存时向其发送消息:对于暂停生命周期抓取器,直到调试目标恢复;或者对于线程生命周期抓取器,直到线程与之分离或退出。但是,与对象抓取器 Actor 不同,客户端可以在 Actor 生存的任何时间与长字符串抓取器 Actor 通信,而不管调试目标是否已暂停。(由于字符串在 JavaScript 中是不可变的值,因此长字符串抓取器 Actor 的响应不能依赖于调试目标的操作。)

抓取器生命周期

大多数抓取器是**暂停生命周期**抓取器:它们仅在 JavaScript 线程暂停时持续存在,并在调试器允许线程恢复执行后立即失效。(暂停生命周期抓取器中的 Actor 是 Actor 的子级,该 Actor 在线程恢复时关闭或与之分离。)此安排允许协议在响应中自由使用抓取器,而无需客户端记住并关闭所有抓取器。

然而,在某些情况下,客户端可能希望在调试对象运行时保留对对象或长字符串的引用。例如,一个显示用户选择的对象的面板必须在每次调试对象暂停时更新其对对象的视图。为了实现这一点,客户端可以将暂停生命周期句柄提升为**线程生命周期**句柄,该句柄持续到线程与调试器分离或退出。线程生命周期句柄中的参与者是线程参与者的子参与者。当客户端不再需要线程生命周期句柄时,可以显式释放它。

暂停生命周期句柄和线程生命周期句柄都是垃圾回收根。

要将暂停生命周期句柄提升为线程生命周期句柄,客户端发送以下格式的数据包

{ "to":<gripActor>, "type":"threadGrip" }

其中gripActor是来自现有暂停生命周期句柄的参与者。句柄参与者将回复

{ "from":<gripActor>, "threadGrip":<threadGrip> }

其中threadGrip是对同一对象的新的句柄,但其参与者由线程参与者而不是暂停参与者作为父级。

客户端可以通过向句柄参与者发送以下格式的请求来释放线程生命周期句柄

{ "to":<gripActor>, "type":"release" }

句柄参与者将简单地回复

{ "from":<gripActor> }

这将关闭句柄参与者。 "release" 数据包只能发送到线程生命周期句柄参与者;如果暂停生命周期句柄参与者接收到 "release" 数据包,它将发送以下格式的错误回复

{ "from":<gripActor>, "error":"notReleasable", "message":<message> }

其中每个gripActor是应释放的thread子参与者的名称。线程参与者将简单地回复

{ "from":<thread> }

无论句柄的生命周期如何,客户端只能在所属线程暂停时向对象句柄参与者发送消息;客户端与可变值的交互不能与线程并发进行。

完成值

某些数据包使用**完成值**描述堆栈帧的执行完成方式,完成值采用以下格式之一

{ "return":<grip> }

这表示帧正常完成,并返回由grip给出的值。

{ "throw":<grip> }

这表示帧抛出了异常;grip是抛出的异常值。

{ "terminated":true }

这表示帧的执行被终止,例如由于“脚本运行缓慢”对话框或内存不足。

源代码位置

许多数据包引用源代码中的特定位置:断点请求指定断点应设置在何处;堆栈帧显示当前执行点;等等。

源代码位置的描述(在数据包描述中写为location)可以采用以下格式之一

{ "url":<url>, "line":<line>, "column":<column> }

这指的是从url加载的源代码的第line行,第column列。行号和列号从1开始。如果省略了columnline,则默认为1。

{ "eval":<location>, "id":<id>, "line":<line>, "column":<column> }

这指的是传递给在location处调用eval的源代码的第line行,第column列。为了区分传递给eval的不同文本,每个文本都分配一个唯一的整数id

{ "function":<location>, "id":<id>, "line":<line>, "column":<column> }

这指的是传递给在location处调用Function构造函数的源代码的第line行,第column列。为了区分传递给Function构造函数的不同文本,每个文本都分配一个唯一的整数id

如上所述,位置可以嵌套。像这样的位置

{ "eval":{ "eval":{ "url":"file:///home/example/sample.js", "line":20 }
           "id":300, "line":30 }
  "id":400, "line":40 }

指的是传递给在file:///home/example/sample.js的第20行上调用eval的代码的第30行上发生的eval调用的代码的第40行。

根参与者

当连接到服务器时,根参与者将使用以下数据包打开对话

{ "from":"root", "applicationType":<appType>, "traits":<traits>, ...}

根参与者的名称始终为"root"appType是一个字符串,指示服务器代表哪种程序。根据appType,可能存在更多属性。

traits是一个对象,描述了此服务器支持但客户端无法通过其他方式方便检测的协议变体。存在的属性名称指示服务器具有哪些特性;属性的值取决于其名称。如果traits没有属性,则可以完全省略数据包的"traits"属性。此版本的协议未定义任何特性,因此,如果"traits"属性存在,则其值必须是一个没有属性的对象{}

对于Web浏览器,介绍性数据包应具有以下格式

{ "from":"root", "applicationType":"browser", "traits":<traits> }

列出浏览器选项卡

要获取浏览器中当前存在的选项卡列表,客户端向根参与者发送以下格式的请求

{ "to":"root", "type":"listTabs" }

根参与者回复

{ "from":"root", "tabs":[<tab>, ...], "selected":<selected> }

其中每个tab描述一个打开的选项卡,而selected是当前选定选项卡在选项卡数组中的索引。此格式可能具有描述其他全局参与者的其他属性;例如,请参阅Chrome调试

每个tab具有以下格式

{ "actor":<targetActor>, "title":<title>, "url":<URL> }

其中targetActor是表示选项卡的参与者的名称,而titleURL是当前在该选项卡中可见的网页的标题和URL。此格式可能具有描述其他特定于选项卡的参与者的其他属性。

要附加到targetActor,客户端发送以下格式的消息

{ "to":<targetActor>, "type":"attach" }

目标参与者回复

{ "from":<targetActor>, "threadActor":<tabThreadActor> }

其中tabThreadActor是表示选项卡当前内容的类似线程的参与者的名称。如果用户导航选项卡,则tabThreadActor将切换到新内容;我们不会为选项卡访问的每个页面创建单独的类似线程的参与者。

如果用户在客户端附加到选项卡之前关闭了选项卡,则targetActor将回复

{ "from":<targetActor>, "error":"exited" }

当客户端不再有兴趣与选项卡交互时,客户端可以请求

{ "to":<targetActor>, "type":"detach" }

targetActor回复

{ "from":<targetActor>, "type":"detached" }

如果客户端尚未附加到targetActor,则targetActor将发送以下格式的错误回复

{ "from":<targetActor>, "error":"wrongState" }

在客户端附加期间,每当用户将选项卡导航到新页面时,targetActor都会向客户端发送通知。导航开始时,targetActor发送以下格式的数据包

{ "from":<targetActor>, "type":"tabNavigated", "state":"start",
  "url":<newURL> }

这表示选项卡已开始导航到newURL;选项卡先前页面中的JavaScript执行将暂停。导航完成后,targetActor发送以下格式的数据包

{ "from":<targetActor>, "type":"tabNavigated", "state":"stop",
  "url":<newURL>, "title":<newTitle> }

其中newURLnewTitle是选项卡现在显示的页面的URL和标题。对原始"attach"数据包的响应中给出的tabThreadActor现在正在调试新页面的代码。

Chrome调试

如果服务器支持调试Chrome代码,则根参与者对"listTabs"请求的回复将包含一个名为"chromeDebugger"的属性,其值为客户端可以附加到其上以调试Chrome代码的类似线程的参与者的名称。

与类似线程的参与者交互

表示独立JavaScript执行线程(如浏览上下文和Web工作线程)的参与者统称为“线程”。与表示线程的参与者的交互遵循更复杂的通信模式。

线程始终处于以下状态之一

  • **分离**:线程正在自由运行,并且目前没有与调试器交互。分离的线程运行、遇到错误并退出,而无需与调试器交换任何消息。调试器可以附加到线程,将其置于**暂停**状态。或者,分离的线程可以自行退出,进入**已退出**状态。

  • **运行**:线程在调试器的观察下运行,执行JavaScript代码或可能被阻塞以等待输入。它将向客户端报告异常、断点命中、监视点命中和其他有趣事件,并进入**暂停**状态。调试器还可以中断正在运行的线程;这会引发响应并将线程置于**暂停**状态。正在运行的线程也可能退出,进入**已退出**状态。

  • **暂停**:线程已向客户端报告暂停,并正在等待进一步的指令。在此状态下,线程可以接受请求并发送回复。如果客户端要求线程继续或单步执行,则它将返回到**运行**状态。如果客户端与线程分离,则它将返回到**分离**状态。

  • **已退出**:线程已停止执行,并将消失。底层线程的资源可能已被释放;此状态仅表示参与者的名称尚不可用于重用。当参与者接收到“release”数据包时,该名称可能会被重用。

Thread states

这些交互旨在具有某些属性

  • 在任何时候,客户端或服务器都不能在没有收到其对应方的数据包的情况下发送无限数量的数据包。这避免了死锁,而无需任何一方为每个参与者缓冲任意数量的数据包。

  • 在调试器或线程都可以启动转换的状态下,调试器始终清楚线程实际进入了哪个状态以及原因。
    例如,如果调试器中断正在运行的线程,则它无法确定线程是否因中断而停止,是否自行暂停(例如报告监视点命中)或退出。但是,调试器收到的下一个数据包将是“paused”或“exited”,从而消除歧义。
    同样,当调试器附加到线程时,它无法确定它是否已成功附加到线程,或者线程是否在“attach”数据包到达之前退出。但是,在这两种情况下,调试器都可以预期一个消除歧义的响应:如果附加成功,它将收到“attached”数据包;在第二种情况下,它将收到“exit”数据包。
    为了支持此属性,线程在某些状态下会忽略某些调试器数据包(例如,在**暂停**和**已退出**状态下的“interrupt”数据包)。这些情况都处理了被某个线程操作抢占的忽略数据包的情况。

请注意,此处的规则适用于客户端与每个线程参与者的单独交互。客户端可以在等待对发送到不同线程参与者的请求的回复的同时,向一个线程参与者发送“interrupt”。

待办事项:显示内容中用户选择节点的情况如何?这些是否应该成为客户端可以在“paused”状态下接收的事件?这对“request”/“reply”模式意味着什么?

附加到线程

要附加到线程,客户端发送以下格式的数据包

{ "to":<thread>, "type":"attach" }

这里,thread是表示线程的参与者,可能是来自“listContexts”回复的浏览上下文。此数据包会导致线程暂停其执行,如果它没有首先自行退出。线程以两种方式之一进行响应

{ "from":<thread>, "type":"paused", "why":{ "type":"attached" }, ... }

线程现在处于**暂停**状态,因为客户端已附加到它。参与者名称thread在客户端与线程分离或确认线程退出之前保持有效。这是一个普通 "paused"数据包,其格式和附加属性如下所述线程暂停

{ "from":<thread>, "type":"exited" }

这表示线程在接收到“attach”数据包之前自行退出。线程现在处于**已退出**状态。客户端应随后发送“release”数据包;请参阅退出线程

如果客户端向不在**分离**或**已退出**状态的线程发送"attach"数据包,则参与者将发送以下格式的错误回复

{ "from":<thread>, "error":"wrongState", "message":<message> }

其中message详细说明了线程当时所处的状态(以便于调试调试器)。在这种情况下,线程的状态不受影响。

与线程分离

要与线程分离,客户端发送以下格式的数据包

{ "to":<thread>, "type":"detach" }

线程以三种方式之一进行响应

{ "from":<thread>, "type":"detached" }

这表示客户端已与线程分离。线程现在处于**分离**状态:它可以自由运行,并且不再向客户端报告事件。与thread的通信已关闭,参与者名称可供重用。如果线程处于**暂停**状态,则暂停参与者将关闭(因为暂停参与者是thread的子参与者)。

{ "from":<thread>, "type":"paused", ... }
{ "from":<thread>, "type":"detached" }

此系列数据包表示线程自行暂停(由于“paused”数据包的附加属性中给出的原因),然后才收到“detach”数据包。如上所述,这表示线程处于**分离**状态,刚刚创建的暂停参与者已关闭,并且参与者名称可供重用。

{ "from":<thread>, "type":"exited" }

这表示线程在接收到“detach”数据包之前自行退出。客户端应随后发送“release”数据包;请参阅退出线程

与线程分离会导致所有断点、监视点和其他与调试相关的状态被遗忘。

如果客户端向不在**运行**、**暂停**或**已退出**状态的线程发送"detach"数据包,则参与者将发送以下格式的错误回复

{ "from":<thread>, "error":"wrongState", "message":<message> }

其中message详细说明了线程当时所处的状态(以便于调试调试器)。在这种情况下,线程的状态不受影响。

运行线程

一旦客户端附加到线程,它就处于**运行**状态。在此状态下,可能会发生四件事

  • 线程可能会命中断点或监视点,或者遇到客户端感兴趣的其他条件。

  • 线程可能会退出。

  • 客户端可能会与线程分离。

  • 客户端可能会中断正在运行的线程。

请注意,客户端操作可能与线程操作同时发生。该协议旨在避免客户端和线程同时操作时的歧义。

线程暂停

如果线程暂停以向客户端报告有趣事件,则它将发送以下格式的数据包

{ "from":<thread>, "type":"paused", "actor":<pauseActor>, "why":<reason>,
  "currentFrame":<frame> }

这表示线程已进入**暂停**状态,并解释了暂停的位置和原因。

PauseActor是一个“暂停执行器”,代表线程的这个特定暂停;它一直存在,直到线程下次离开**暂停**状态。暂停执行器是引用在此暂停期间发现的值和其他实体的执行器的父执行器;当线程恢复时,这些执行器会自动关闭。这减轻了客户端显式关闭暂停期间提到的每个执行器的责任。

由于值握柄中的执行器由暂停执行器作为父执行器,这意味着当线程恢复或与之分离时,这些握柄将失效;无法从一个暂停中获取握柄并在下一个暂停中使用它。要创建在暂停之间保持有效的握柄,请参阅握柄生命周期

currentFrame值描述了 JavaScript 调用栈顶部的帧;请参阅下面的列出调用栈帧

reason值描述了线程暂停的原因。它具有以下其中一种形式

{ "type":"attached" }

线程暂停是因为客户端已附加到它。

{ "type":"interrupted" }

线程停止是因为它从客户端收到了一个“中断”数据包。

{ "type":"resumeLimit" }

客户端使用包含resumeLimit属性的"resume"数据包恢复了线程,并且线程暂停是因为满足了给定的limit。执行仍然保留在恢复线程的帧中,并且该帧不会即将弹出。

{ "type":"resumeLimit", "frameFinished":<completion> }

客户端使用包含resumeLimit属性的"resume"数据包恢复了线程,并且线程暂停是因为该帧即将弹出。Completion是一个完成值,描述了帧的执行如何结束。即将弹出的帧仍然是栈顶部的帧,但后续的"resume"操作将在调用帧中运行。

{ "type":"debuggerStatement" }

线程停止是因为它执行了 JavaScript 的“debugger”语句。

{ "type":"breakpoint", "actors":[<breakpointActor>...] }

线程在给定执行器表示的断点处停止。

{ "type":"watchpoint", "actors":[<watchpointActor>...] }

线程在给定执行器表示的监视点处停止。

待办事项:这应该在数据包中提供有关监视点的更多详细信息,而不是在我们能够显示任何有用的信息之前进行另一轮往返。

{ "type":"clientEvaluated", "frameFinished":<completion> }

客户端先前clientEvaluate命令中给出的表达式已完成执行;completion是一个完成值,描述了它是如何完成的。clientEvaluate恢复创建的帧已从栈中弹出。有关详细信息,请参阅评估源语言表达式

恢复线程

如果线程处于**暂停**状态,则客户端可以通过发送以下形式的数据包来恢复它

{ "to":<thread>, "type":"resume" }

这将使线程进入**运行**状态。线程将再次因断点命中、监视点命中、抛出监视、帧弹出监视和其他待定的暂停请求而暂停。

要单步执行线程的执行,客户端可以发送以下形式的数据包

{ "to":<thread>, "type":"resume", "resumeLimit":<limit> }

Limit必须具有以下其中一种形式

{ "type":"next" }

线程应暂停

  • 就在当前帧弹出之前,无论是通过抛出异常还是返回值;或者

  • 当当前帧中的控制到达与当前所在的语句不同的语句时。

请注意,比当前帧更年轻的帧中的执行永远不会满足这些条件,因此"next"限制会跳过调用、生成器迭代器调用等。

{ "type":"step" }

线程应暂停

  • 就在当前帧弹出之前,无论是通过抛出异常还是返回值;或者

  • 就在新帧入栈之后;或者

  • 当当前帧中的控制到达与当前所在的语句不同的语句时。

这与"next"相同,只是它会步入调用。

要恢复线程但使其在当前帧即将弹出时停止,客户端可以发送以下形式的数据包

{ "to":<thread>, "type":"resume", "resumeLimit":{ "type":"finish" } }

在这里,线程应该在当前帧弹出之前暂停,无论是通过抛出异常、返回值还是终止。

当线程因达到限制而暂停时,“paused”数据包的reason将具有"resumeLimit"类型。

恢复限制仅适用于当前恢复;一旦线程暂停,无论是由于达到限制还是发生了其他事件(例如断点命中),恢复限制将不再有效。

如果"resume"数据包中没有出现"resumeLimit"属性,则线程应运行,直到满足某些待定暂停条件(断点命中;监视点触发;或类似情况)。

要强制当前帧立即结束执行,客户端可以发送以下形式的数据包

{ "to":<thread>, "type":"resume", "forceCompletion":<completion> }

其中completion是一个完成值,指示帧是否应返回值、抛出异常或终止。执行在当前帧的调用方恢复,以适合completion的方式。

要请求在抛出异常时暂停执行,客户端可以发送以下形式的请求

{ "to":<thread>, "type":"resume", "pauseOnExceptions": true }

如果pauseOnExceptions的值为false或省略,则在抛出异常时执行将继续。当线程因抛出异常而暂停时,“paused”数据包的reason将具有以下形式

{ "type":"exception", "exception":<exception> }

其中exception是对异常对象的握柄。

要请求在 DOM 事件上暂停执行,客户端可以发送以下形式的请求

如果"resume"数据包中存在"forceCompletion"属性,以及"resumeLimit""pauseOnExceptions",则线程将回复错误

{ "from":<thread>, "error":"badParameterType", "message":<message> }

"resume"数据包关闭了客户端在开始暂停的“paused”数据包中提供的暂停执行器。

如果客户端向未处于**暂停**状态的线程发送"resume"数据包,则执行器将发送以下形式的错误回复

{ "from":<thread>, "error":"wrongState", "message":<message> }

其中message详细说明了线程当时所处的状态(以便于调试调试器)。在这种情况下,线程的状态不受影响。

中断线程

如果线程处于**运行**状态,则客户端可以通过发送以下形式的数据包使其在当前位置暂停

{ "to":<thread>, "type":"interrupt" }

线程以两种方式之一进行响应

{ "from":<thread>, "type":"paused", "why":<reason>, ... }

这表示线程已停止,现在处于**暂停**状态。如果reason{ "type":"interrupted" },则线程由于客户端的interrupt数据包而暂停。否则,线程在收到interrupt数据包之前自行暂停,并在收到它时忽略interrupt数据包。在任何一种情况下,这都是一个普通"paused"数据包,其形式和附加属性如上文线程暂停中所述。

{ "from":<thread>, "type":"exited" }

这表示线程在收到客户端的interrupt数据包之前已退出,现在处于**退出**状态。请参阅下面的退出线程

如果客户端向未处于**运行**、**暂停**或**退出**状态的线程发送"interrupt"数据包,则执行器将发送以下形式的错误回复

{ "from":<thread>, "error":"wrongState", "message":<message> }

其中message详细说明了线程当时所处的状态(以便于调试调试器)。在这种情况下,线程的状态不受影响。

退出线程

当处于**运行**状态的线程退出时,它会发送以下形式的数据包

{ "from":<thread>, "type":"exited" }

此时,客户端无法再操作线程,并且线程的大部分资源可能会被释放;但是,线程执行器名称必须保持活动状态,以处理杂散的interruptdetach数据包。为了允许释放线程的最后痕迹,客户端应发送以下形式的数据包

{ "to":<thread>, "type":"release" }

这确认了退出并允许将线程执行器名称thread重用于其他执行器。

检查暂停的线程

当线程处于**暂停**状态时,调试器可以发出请求以检查其栈、词法环境和值。

只有明确定义为执行此操作的数据包才能导致线程恢复执行。JavaScript 功能(例如 getter、setter 和代理),通常会导致检查操作(例如枚举属性和检查其值)运行任意 JavaScript 代码,在线程暂停时会被禁用。如果给定的协议请求未定义为让线程运行,但执行请求的操作通常会导致它运行(例如,获取 getter 属性的值),则执行器将发送以下形式的错误回复

{ "from":<actor>, "error":"threadWouldRun", "message":<message>, "cause":<cause> }

其中message是可以显示给用户的文本,解释了为什么无法执行该操作。Cause是以下字符串之一

cause 含义
"proxy" 执行该操作将导致代理处理程序运行。
"getter" 执行该操作将导致对象属性 getter 运行。
"setter" 执行该操作将导致对象属性 setter 运行。

"threadWouldRun"错误名称和cause值一起应允许调试器显示适当的本地化错误消息。)

加载脚本源

要获取线程执行器当前加载的所有源的快照,客户端可以发送以下数据包

{ to: <threadActorID>, type: "sources" }

响应数据包具有以下形式

{ from: <threadActorID>, sources: [<sourceForm1>, <sourceForm2>, ..., <sourceFormN>] }

其中每个sourceForm具有以下形式

{ actor: <sourceActorID>,
  url: <sourceURL>,
  isBlackBoxed: <isBlackBoxed> }
  • sourceActorID是源执行器的 ID

  • sourceURL是源执行器表示的源的 URL

  • isBlackBoxed是一个布尔值,指定源执行器的“黑盒”标志是否已设置。请参阅黑盒源

每个源执行器在整个线程的生命周期中都存在。

要获取源的内容,请向相应的源执行器发送以下数据包

{ to: <sourceActorID>, type: "source" }

源执行器将回复以下形式的数据包

{ from: <sourceActorID>, source: <contentsOfSource> }

其中contentsOfSource是一个表示源代码字符串的握柄:JSON 字符串或长字符串握柄。(有关长字符串握柄的描述,请参阅握柄。)

黑盒源

在调试使用大型现成 JavaScript 库的 Web 应用程序时,将此类库视为“黑盒”可能有助于开发人员专注于自己的代码,其内部细节在用户界面中被省略或简化。例如,用户界面可以将黑盒库内的调用栈子链显示为单个元素;在黑盒库中设置的断点可以被禁用;等等。

每个源执行器都有一个“黑盒”标志,并且可以理解设置和清除该标志的请求。当源执行器被黑盒化时,调试器在命中该源内部的断点或debugger语句时不会暂停。如果启用了暂停异常,并且在黑盒源内部抛出异常,则调试器不会暂停,直到栈已展开到未被黑盒化的源中的帧。

线程执行器仍然在"sources"回复中列出黑盒源执行器;并在"frames"请求中包含运行黑盒代码的调用栈帧。但是,每个sourceForm都包含一个"isBlackBoxed"属性,为客户端提供了在用户界面中实现黑盒行为所需的所有信息。

要设置源执行器的“黑盒”标志

{ "to": <sourceActor>, "type": "blackbox" }

sourceActor在成功时将回复一个空白响应

{ "from": <sourceActor> }

或在失败时回复错误响应

{ "from": <sourceActor>, "error": <reason> }

要清除源执行器的“黑盒”标志

{ "to": <sourceActor>, "type": "unblackbox" }

同样,sourceActor在成功时将回复一个空白响应

{ "from": <sourceActor> }

或在失败时回复错误响应

{ "from": <sourceActor>, "error": <reason> }

列出调用栈帧

要检查线程的 JavaScript 调用栈,客户端可以发送以下请求

{ "to":<thread>, "type":"frames", "start":<start>, "count":<count> }

startcount属性是可选的。如果存在,则start给出回复应描述的最年轻的调用栈帧的编号,其中栈中最年轻的帧为帧号零;如果不存在,则start被视为零。如果存在,则count指定回复应描述的最大帧数;如果不存在,则被视为无穷大。(客户端可能应该避免发送没有countframes请求,以避免被无限递归中的帧淹没。)

线程回复如下

{ "from":<thread>, "frames":[<frame> ...] }

其中每个frame具有以下形式

{ "actor": <actor>,
  "depth": <depth>,
  "type": <type>,
  "this": <this>,
  ... }

其中

  • actor是表示此帧的执行器的名称;

  • depth是此帧的编号,从栈中最年轻的帧的零开始;

  • type是一个字符串,指示此帧是什么类型的;以及

  • this是对此调用的this值的握柄。

帧可能具有其他属性,具体取决于type

帧中提到的所有执行器或帧中出现的握柄(actorcalleeenvironment等)都是由线程执行器作为父执行器。

全局代码帧

全局代码的帧具有以下形式

{ "actor":<actor>,
  "depth":<depth>,
  "type":"global",
  "this":<this>,
  "where":<location>,
  "source":<source>,
  "environment":<environment> }

其中

  • location是全局代码中当前执行点的源位置(请参阅源位置);

  • environment是一个表示当前执行点的词法环境的值(请参阅词法环境);

  • source 是一个源表单,如加载脚本源中所述。

以及其他属性如上所述。

函数调用帧

普通 JavaScript 函数调用的帧具有以下形式

{ "actor":<actor>, "depth":<depth>, "type":"call", "this":<this>,
  "where":<location>, "environment":<environment>,
  "callee":<callee>, "arguments":<arguments> }

其中

  • callee 是对正在调用的函数值的抓取;

  • arguments 是一个对传递给函数的实际值的抓取的数组;

以及其他属性如上所述。

如果被调用者是宿主函数,或作用域与我们附加到的全局范围不同的某个全局范围的函数,则"where""environment" 属性不存在。

由于各种原因,参数列表可能不完整或不准确。如果程序已将其分配给其形式参数,则可能已丢失传递的原始值,并且编译器优化可能会删除一些参数值。

Eval 帧

eval 的调用的帧具有以下形式

{ "actor":<actor>, "depth":<depth>, "type":"eval", "this":<this>,
  "where":<location>, "environment":<environment> }

其中属性如上所定义。

客户端评估帧

当客户端使用 clientEvaluate 数据包评估表达式时,评估将作为一种特殊类型的帧出现在堆栈上,其形式为

{ "actor":<actor>, "depth":<depth>, "type":"clientEvaluate", "this":<this>,
  "where":<location>, "environment":<environment> }

其中属性如上所定义。在这种情况下,where 将是调试器提供的表达式内的位置。

弹出堆栈帧

客户端可以通过发送以下形式的请求从堆栈中删除帧

{ "to":<frameActor>, "type":"pop", "completionValue":<completion> }

其中 frameActor 是表示要弹出的堆栈帧的 actor,而 completion 是一个完成值,描述帧应如何显示已完成执行。所有较年轻的堆栈帧也将被弹出。线程保持暂停状态。帧 actor 将回复

{ "from":<frameActor>, "watches":[<watchActor> ...] }

其中每个 watchActor 是在弹出给定帧的过程中触发的帧弹出监视 actor 的名称。如果没有触发帧弹出监视,则可以省略 watches 属性。

待办事项:指定如果无法弹出帧则返回的错误 - 是否可以弹出宿主 (C++) 函数帧?

评估源语言表达式

要在线程中评估源语言表达式,客户端会发送以下形式的专用 "resume" 数据包

{ "to":<thread>, "type":"clientEvaluate", "expression":<expr>, "frame":<frame> }

这将恢复线程,就像普通的 "resume" 数据包一样,但不是在暂停发生的地方继续执行,而是让线程开始评估由 expr(一个字符串)给出的源语言表达式。评估发生在一个新的客户端评估帧中,推送到 thread 的当前堆栈顶部,使用 frame 的环境。Frame 必须是 thread 的帧之一的活动 actor,并且给定的帧必须是我们从中可以检索词法环境的帧;也就是说,它不能是针对非被调试函数调用的帧。当 expr 的评估完成后,客户端将报告包含表达式值的 clientEvaluate 暂停。

如果 expr 的评估突然完成,此结果仍将通过 clientEvaluated 暂停报告,因此客户端无需采取明确步骤来捕获表达式抛出的异常。

如果 frame 不是当前在 thread 堆栈上的帧的 actor 的名称,则线程 actor 将发送以下形式的回复

{ "from":<thread>, "error":"unknownFrame", "message":<message> }

其中 message 提供对调试器开发人员有帮助的任何详细信息。在这种情况下,线程的状态不受影响。

如果 frame 不是我们可以访问其环境的帧,则线程 actor 将发送以下形式的错误回复

{ "from":<thread>, "error":"notDebuggee", "message":<message> }

其中 message 提供更多适当的详细信息。

如果客户端向未处于**已暂停**状态的线程发送 "clientEvaluate" 数据包,则 actor 将发送以下形式的错误回复

{ "from":<thread>, "error":"wrongState", "message":<message> }

其中message详细说明了线程当时所处的状态(以便于调试调试器)。在这种情况下,线程的状态不受影响。

待办事项:使用给定的抓取绑定到给定的标识符进行评估

词法环境

词法环境(在数据包描述中写为 environment)记录程序中特定点可见的标识符绑定。环境具有以下形式之一

{ "type":"object", "actor":<actor>, "object":<object>, "parent":<parentEnvironment> }

这表示一个作用域链元素,其标识符绑定反映了 object(一个抓取)的属性。这可能是全局对象(在浏览器中为 window),或 DOM 元素(对于事件处理程序内容属性,这些属性在其作用域链上具有输入元素、表单和文档以及 window)。

Actor 是表示此词法环境的 actor 的名称。它可以回答的请求将在下面描述。

ParentEnvironment 是一个词法环境,描述了下一个封闭的环境;在最外层环境中省略了 parent 属性。

{ "type":"function", "actor":<actor>, "function":<function>,
  "bindings":<bindings>, "parent":<parentEnvironment> }

这表示由对 function(一个抓取)的调用创建的变量环境。Bindings 描述了作用域内的绑定,包括函数的参数、arguments 对象以及局部 var 和函数绑定;其形式将在下面详细描述。其他属性如上所述。

{ "type":"with", "actor":<actor>, "object":<object>, "parent":<parentEnvironment> }

这表示由操作数为 object(一个抓取)的 with 语句引入的环境。其他属性如上所述。

{ "type":"block", "actor":<actor>, "bindings":<bindings>, "parent":<parentEnvironment> }

这表示由 let 块、for-in 语句、catch 块等引入的环境。属性如上所述。

bindings 值具有以下形式

{ "arguments":[ { name:<descriptor> }, ... ],
  "variables":{ name:<descriptor>, ... } }

每个 name 都是绑定标识符的名称,作为字符串。每个 descriptor 都是变量的属性描述符,将变量的值作为描述符的 "value" 属性,并将变量的可变性作为描述符的 "writable" 属性。描述符的 "configurable" 属性反映了环境是否支持删除和添加变量。每个描述符的 "enumerable" 属性为 true

"arguments" 列表仅出现在 "function" 环境的绑定中。它按它们在函数定义中出现的顺序列出参数。(根据 JavaScript 的允许,同一个名称可能在列表中出现多次;名称的最后一次出现是在函数中处于作用域的名称。)

请注意,语言实现可能会从函数的作用域中省略一些环境记录,如果它可以确定函数不会使用它们。这意味着调试器可能无法找到应该在作用域内的所有变量。

要完全枚举任何词法环境引入的绑定,客户端可以向环境的 actor 发送以下形式的请求

{ "to":<envActor>, "type":"bindings" }

actor 将如下回复

{ "from":<envActor>, "bindings":<bindings> }

请注意,当 envActor 指向其对象为代理的对象环境时,此请求会引发 "threadWouldRun" 错误回复。

要更改在特定词法环境中绑定的变量的值,客户端可以向环境的 actor 发送请求

{ "to":<envActor>, "type":"assign", "name":<name>, "value":<value> }

这会将名称为 name(一个字符串)的标识符的值更改为 value(一个抓取)所表示的值。actor 将如下回复,简单地

{ "from":<envActor> }

如果命名的标识符是不可变的,则 actor 将发送以下形式的错误回复

{ "from":<envActor>, "error":"immutableBinding", "message":<message> }

如果 envActor 指向其对象为代理的对象环境,或其名为 name 的属性具有 setter 函数,则此请求会引发 "threadWouldRun" 错误回复。

词法环境示例

例如,如果我们有以下 JavaScript 代码

function f(x) {
  function g(y) {
    var z = "value of z";
    alert(x + y);
  }
}

我们在包含对 alert 的调用的行上设置断点,然后评估表达式

f("argument to f")("argument to g")

那么我们将遇到该断点,引发如下数据包

{ "from":<thread>, "type":"paused", "actor":<pauseActor>,
  "why":{ "type":"breakpoint", "actors":[<breakpointActor>] },
  "frame":{ "actor":<frameActor>, "depth":1,
            "type":"call", "where":{ "url":"sample.js", "line":3 },
            "environment":{ "type":"function", "actor":<gFrameActor>,
                            "function":{ "type":"object", "class":"Function", "actor":<gActor> },
                            "functionName":"g",
                            "bindings":{ arguments: [ { "y": { "value":"argument to g", "configurable":"false",
                                                               "writable":true, "enumerable":true } } ] },
                            "parent":{ "type":"function", "actor":<fFrameActor>,
                                       "function":{ "type":"object", "class":"Function", "actor":<fActor> },
                                       "functionName":"f",
                                       "bindings": { arguments: [ { "x": { "value":"argument to f", "configurable":"false",
                                                                    "writable":true, "enumerable":true } } ],
                                                     variables: { "z": { "value":"value of z", "configurable":"false",
                                                                         "writable":true, "enumerable":true } } },
                                       "parent":{ "type":"object", "actor":<globalCodeActor>,
                                                  "object":{ "type":"object", "class":"Global",
                                                             "actor":<globalObjectActor> }
                                                }
                                     }
                          },
                       "callee":<gActor>, "calleeName":"g",
            "this":{ "type":"object", "class":"Function", "actor":<gActor> },
            "arguments":["argument to g"]
          }
}

您可以在此处看到三种嵌套的环境形式,从顶部堆栈帧的 environment 属性开始,在暂停中报告

  • 第一种环境形式显示了由对 g 的调用创建的环境记录,其中将字符串 "argument to g" 作为 y 的值传递。

  • 因为 g 嵌套在 f 中,所以为 g 生成的每个函数对象都捕获对封闭函数 f 的调用的环境。因此,g 的作用域链上的下一项是对 f 的调用的环境形式,其中将 "argument to f" 作为 x 的值传递。

  • 因为 f 是顶级函数,所以 f 的(唯一)函数对象会闭包全局对象。这是显示为 f 的环境记录的父级的“type”:”object”环境。

  • 因为全局对象位于作用域链的末尾,所以其环境形式没有 parent 属性。

断点

当线程暂停时,客户端可以通过发送以下形式的请求在线程的代码中设置断点

{ "to":<thread>, "type":"setBreakpoint", "location":<location> }

其中 location 是一个源位置。如果线程能够在给定位置建立断点,它将回复

{ "from":<thread>, "actor":<actor>, "actualLocation":<actualLocation> }

其中 actor 是表示断点的 actor(线程 actor 的子级),而 actualLocation 是实际设置断点的位置。如果 locationactualLocation 相同,则可以省略 actualLocation 属性。

如果线程找不到 location 中引用的脚本,它将发送以下形式的错误回复

{ "from":<thread>, "error":"noScript" }

如果 location 指向给定脚本没有程序代码的行和列,并且无法选择合理的替代位置(例如,通过跳过),则线程将发送以下形式的错误回复

{ "from":<thread>, "error":"noCodeAtLineColumn" }

要删除断点,客户端可以向断点的 actor 发送以下形式的消息

{ "to":<breakpointActor>, "type":"delete" }

断点 actor 将简单地回复

{ "from":<breakpointActor> }

这将关闭与 breakpointActor 的通信。

事件侦听器

要请求附加到页面的所有事件侦听器和事件处理程序的列表(有关这两个术语的定义,请参阅DOM 事件处理程序),客户端会发送以下形式的请求

{ "to":<thread>, "type":"eventListeners" }

线程将回复以下形式的响应

{ "from":<thread>, "listeners":[ <listener>, ... ] }

此类请求可以在线程暂停或运行时发送。listener 值具有以下形式

{ "node":{ "selector":<node-selector>, "object":<node> },
  "type":<type>,
  "capturing":<capturing>,
  "allowsUntrusted":<allowsUntrusted>,
  "inSystemEventGroup":<inSystemEventGroup>,
  "isEventHandler":<isEventHandler>,
  "function":<function> }

这些属性的值为

node-selector

附加事件处理程序的 DOM 元素的唯一 CSS 选择器,如果处理程序附加在窗口上,则为 "window"

node

对附加事件处理程序的 DOM 元素的抓取。

type

DOM 规范中指定的 DOM 事件类型(请参阅nsIEventListenerInfo)。

capturing

一个布尔标志,指示事件侦听器是否处于捕获模式(请参阅nsIEventListenerInfo)。

allowsUntrusted

一个布尔标志,指示侦听器是否允许不受信任的事件(请参阅nsIEventListenerInfo)。

inSystemEventGroup

一个布尔标志,指示事件侦听器是否在系统事件组中(请参阅nsIEventListenerInfo)。

isEventHandler

一个布尔标志,指示这是否是事件处理程序或事件侦听器(有关这两个术语的定义,请参阅DOM 事件处理程序)。对于 HTML 属性处理程序或对 WebIDL 属性的赋值,此标志将为真。

function

对函数对象的抓取。

流传输

调试协议以客户端和服务器之间交换的数据包来指定,其中每个数据包要么是 JSON 文本,要么是一块字节(“批量数据”数据包)。协议没有指定任何特定机制来将数据包从一方传输到另一方。实现可以选择任何他们喜欢的传输方式,只要数据包可靠、完整且按顺序到达即可。

本节描述了 Mozilla 远程调试协议流传输,这是一种适用于通过可靠的有序字节流(如 TCP/IP 流或管道)传输 Mozilla 调试协议数据包的传输层。调试器用户界面可以使用它与其他进程中的被调试对象(例如,用于调试 Firefox 浏览器代码)或其他机器(例如,用于调试在手机或平板电脑上运行的 Firefox OS 应用程序)交换数据包。

(流传输不是 Mozilla 使用的唯一传输方式。例如,当使用 Firefox 的内置脚本调试器时,客户端和服务器位于同一进程中,因此为了提高效率,它们使用一种传输方式,该传输方式仅交换对应于协议指定的 JSON 文本的 JavaScript 对象,并完全避免序列化数据包。)

数据包

一旦建立了底层字节流,传输参与者就可以立即开始发送数据包,使用此处描述的格式。传输不需要任何初始握手或设置,也不需要关闭交换:每个方向上流中的第一个字节是第一个数据包的字节(如果有);每个方向上流中的最后一个字节是最后一个发送的数据包的最后一个字节(如果有)。

传输定义了两种类型的数据包:JSON 和批量数据。

JSON 数据包

JSON 数据包具有以下格式

length:JSON

其中 length 是十进制 ASCII 数字的序列,JSON 是一个格式良好的 JSON 文本(如 RFC 4627 中定义的),以 UTF-8 编码,并且 length(解释为数字)是 JSON 的字节长度。

批量数据包

批量数据包具有以下格式

bulk actor type length:data

其中

  • 关键字 bulk 以 ASCII 编码,空格始终恰好是一个 ASCII 空格

  • actor 是 Unicode 字符序列,以 UTF-8 编码,不包含空格或冒号

  • type 是 Unicode 字符序列,以 UTF-8 编码,不包含空格或冒号

  • length 是十进制 ASCII 数字序列

  • data 是字节序列,其长度为 length 解释为数字

actor 字段是发送或接收数据包的 actor 的名称。(actor 是服务器端实体,因此如果数据包是由客户端发送的,则 actor 指定接收者;如果数据包是由服务器发送的,则 actor 指定发送者。)协议对 actor 名称施加了与我们此处要求相同的语法限制。

在交换的任何给定时间点,哪些 actor 名称有效是由远程调试协议确定的。

type 字段定义数据包的类型,可将其与 actor 名称一起使用以将数据包正确路由到其目标。协议提供了有关类型的更多详细信息,这些详细信息在此处仍然有效。

批量数据包的内容恰好是作为 data 出现的字节序列。数据不是 UTF-8 文本。

流要求

流传输要求底层流具有以下属性

  • 它必须是透明的:每个传输的字节都会在未修改的情况下传递给接收方。值为 ASCII 控制字符或完全超出 ASCII 范围的字节必须保持不变;换行符保持不变。

  • 它必须是可靠的:每个传输的字节都到达接收方,否则连接将完全断开。例如,由硬件引入的错误必须被检测和纠正,或者至少报告(并且连接断开)。流传输本身不包含任何校验和;这些是流的责任。(因此,例如,普通的串行线不适合用作底层流。)

  • 它必须是有序的:字节按传输的顺序接收,并且字节不会重复。(例如,UDP 数据包可能会重复或乱序到达。)

TCP/IP 流和 USB 流满足这些要求。

实现说明

恒定开销的批量数据

Mozilla 向协议中添加了批量数据包,以使内存有限的设备能够更有效地上传性能分析和其他大型数据集。性能分析数据集需要尽可能大,因为更大的数据集可以覆盖更长的时间段或更频繁的样本。但是,将大型数据集转换为 JavaScript 对象,将该对象转换为 JSON 文本,然后通过连接发送文本需要创建数据的几个临时完整副本;在小型设备上,这限制了探查器可以收集的数据量。避免这些临时副本将允许小型设备收集和传输更大的性能分析数据集。由于其他类型的工具似乎也需要有效地交换大型二进制块,因此我们希望找到一个所有协议参与者都可以使用的解决方案,而不是针对探查器的特定情况量身定制的解决方案。

在我们对这种流传输的实现中,当参与者希望传输批量数据包时,它会提供 actor 名称、类型、数据的字节长度和回调函数。当底层流准备好发送更多数据时,传输会写入数据包的 bulk actor type length: 标头,然后将底层 nsIOutputStream 传递给回调函数,然后回调函数将数据包的数据部分直接写入流。类似地,当参与者接收批量数据包时,传输会解析标头,然后将 actor 名称、类型和传输的底层 nsIInputStream 传递给回调函数,该函数直接使用数据。因此,虽然回调函数很可能会使用固定大小的缓冲区来发送和接收数据,但传输不会施加与数据完整大小成正比的开销。