Bug 694539: Implement 'long string' grips. r=jimb

This commit is contained in:
Nick Fitzgerald 2012-08-30 14:10:07 -07:00
Родитель b8470437f3
Коммит fa9547d0c6
7 изменённых файлов: 497 добавлений и 68 удалений

Просмотреть файл

@ -208,7 +208,8 @@ const DebugProtocolTypes = {
"prototypeAndProperties": "prototypeAndProperties",
"resume": "resume",
"scripts": "scripts",
"setBreakpoint": "setBreakpoint"
"setBreakpoint": "setBreakpoint",
"substring": "substring"
};
const ROOT_ACTOR_NAME = "root";
@ -514,6 +515,7 @@ function ThreadClient(aClient, aActor) {
this._actor = aActor;
this._frameCache = [];
this._scriptCache = {};
this._pauseGrips = {};
}
ThreadClient.prototype = {
@ -878,10 +880,6 @@ ThreadClient.prototype = {
* A pause-lifetime object grip returned by the protocol.
*/
pauseGrip: function TC_pauseGrip(aGrip) {
if (!this._pauseGrips) {
this._pauseGrips = {};
}
if (aGrip.actor in this._pauseGrips) {
return this._pauseGrips[aGrip.actor];
}
@ -891,6 +889,22 @@ ThreadClient.prototype = {
return client;
},
/**
* Return an instance of LongStringClient for the given long string grip.
*
* @param aGrip Object
* The long string grip returned by the protocol.
*/
longString: function TC_longString(aGrip) {
if (aGrip.actor in this._pauseGrips) {
return this._pauseGrips[aGrip.actor];
}
let client = new LongStringClient(this._client, aGrip);
this._pauseGrips[aGrip.actor] = client;
return client;
},
/**
* Invalidate pause-lifetime grip clients and clear the list of
* current grip clients.
@ -899,7 +913,7 @@ ThreadClient.prototype = {
for each (let grip in this._pauseGrips) {
grip.valid = false;
}
this._pauseGrips = null;
this._pauseGrips = {};
},
/**
@ -933,9 +947,7 @@ function GripClient(aClient, aGrip)
GripClient.prototype = {
get actor() { return this._grip.actor },
_valid: true,
get valid() { return this._valid; },
set valid(aValid) { this._valid = !!aValid; },
valid: true,
/**
* Request the name of the function and its formal parameters.
@ -1017,6 +1029,45 @@ GripClient.prototype = {
}
};
/**
* A LongStringClient provides a way to access "very long" strings from the
* debugger server.
*
* @param aClient DebuggerClient
* The debugger client parent.
* @param aGrip Object
* A pause-lifetime long string grip returned by the protocol.
*/
function LongStringClient(aClient, aGrip) {
this._grip = aGrip;
this._client = aClient;
}
LongStringClient.prototype = {
get actor() { return this._grip.actor; },
get length() { return this._grip.length; },
valid: true,
/**
* Get the substring of this LongString from aStart to aEnd.
*
* @param aStart Number
* The starting index.
* @param aEnd Number
* The ending index.
* @param aCallback Function
* The function called when we receive the substring.
*/
substring: function LSC_substring(aStart, aEnd, aCallback) {
let packet = { to: this.actor,
type: DebugProtocolTypes.substring,
start: aStart,
end: aEnd };
this._client.request(packet, aCallback);
}
};
/**
* Breakpoint clients are used to remove breakpoints that are no longer used.
*

Просмотреть файл

@ -5,6 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/**
* JSD2 actors.
*/
@ -786,6 +787,11 @@ ThreadActor.prototype = {
*/
createValueGrip: function TA_createValueGrip(aValue) {
let type = typeof(aValue);
if (type === "string" && this._stringIsLong(aValue)) {
return this.longStringGrip(aValue);
}
if (type === "boolean" || type === "string" || type === "number") {
return aValue;
}
@ -872,6 +878,42 @@ ThreadActor.prototype = {
return this.objectGrip(aValue, this.threadLifetimePool);
},
/**
* Create a grip for the given string.
*
* @param aString String
* The string we are creating a grip for.
*/
longStringGrip: function TA_longStringGrip(aString) {
if (!this._pausePool) {
throw new Error("LongString grip requested while not paused.");
}
if (!this._pausePool.longStringActors) {
this._pausePool.longStringActors = {};
}
if (this._pausePool.longStringActors.hasOwnProperty(aString)) {
return this._pausePool.longStringActors[aString].grip();
}
let actor = new LongStringActor(aString, this);
this._pausePool.addActor(actor);
this._pausePool.longStringActors[aString] = actor;
return actor.grip();
},
/**
* Returns true if the string is long enough to use a LongStringActor instead
* of passing the value directly over the protocol.
*
* @param aString String
* The string we are checking the length of.
*/
_stringIsLong: function TA__stringIsLong(aString) {
return aString.length >= DebuggerServer.LONG_STRING_LENGTH;
},
// JS Debugger API hooks.
/**
@ -1020,6 +1062,75 @@ PauseActor.prototype = {
};
/**
* A base actor for any actors that should only respond receive messages in the
* paused state. Subclasses may expose a `threadActor` which is used to help
* determine when we are in a paused state. Subclasses should set their own
* "constructor" property if they want better error messages. You should never
* instantiate a PauseScopedActor directly, only through subclasses.
*/
function PauseScopedActor()
{
}
/**
* A function decorator for creating methods to handle protocol messages that
* should only be received while in the paused state.
*
* @param aMethod Function
* The function we are decorating.
*/
PauseScopedActor.withPaused = function PSA_withPaused(aMethod) {
return function () {
if (this.isPaused()) {
return aMethod.apply(this, arguments);
} else {
return this._wrongState();
}
};
};
PauseScopedActor.prototype = {
/**
* Returns true if we are in the paused state.
*/
isPaused: function PSA_isPaused() {
// When there is not a ThreadActor available (like in the webconsole) we
// have to be optimistic and assume that we are paused so that we can
// respond to requests.
return this.threadActor ? this.threadActor.state === "paused" : true;
},
/**
* Returns the wrongState response packet for this actor.
*/
_wrongState: function PSA_wrongState() {
return {
error: "wrongState",
message: this.constructor.name +
" actors can only be accessed while the thread is paused."
}
}
};
/**
* Utility function for updating an object with the properties of another
* object.
*
* @param aTarget Object
* The object being updated.
* @param aNewAttrs Object
* The new attributes being set on the target.
*/
function update(aTarget, aNewAttrs) {
for (let key in aNewAttrs) {
aTarget[key] = aNewAttrs[key];
}
}
/**
* Creates an actor for the specified object.
*
@ -1034,13 +1145,11 @@ function ObjectActor(aObj, aThreadActor)
this.threadActor = aThreadActor;
}
ObjectActor.prototype = {
actorPrefix: "obj",
ObjectActor.prototype = Object.create(PauseScopedActor.prototype);
WRONG_STATE_RESPONSE: {
error: "wrongState",
message: "Object actors can only be accessed while the thread is paused."
},
update(ObjectActor.prototype, {
constructor: ObjectActor,
actorPrefix: "obj",
/**
* Returns a grip for this actor for returning in a protocol message.
@ -1066,14 +1175,11 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onOwnPropertyNames: function OA_onOwnPropertyNames(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onOwnPropertyNames:
PauseScopedActor.withPaused(function OA_onOwnPropertyNames(aRequest) {
return { from: this.actorID,
ownPropertyNames: this.obj.getOwnPropertyNames() };
},
}),
/**
* Handle a protocol request to provide the prototype and own properties of
@ -1082,11 +1188,8 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onPrototypeAndProperties: function OA_onPrototypeAndProperties(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onPrototypeAndProperties:
PauseScopedActor.withPaused(function OA_onPrototypeAndProperties(aRequest) {
let ownProperties = {};
for each (let name in this.obj.getOwnPropertyNames()) {
try {
@ -1102,7 +1205,7 @@ ObjectActor.prototype = {
return { from: this.actorID,
prototype: this.threadActor.createValueGrip(this.obj.proto),
ownProperties: ownProperties };
},
}),
/**
* Handle a protocol request to provide the prototype of the object.
@ -1110,14 +1213,10 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onPrototype: function OA_onPrototype(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onPrototype: PauseScopedActor.withPaused(function OA_onPrototype(aRequest) {
return { from: this.actorID,
prototype: this.threadActor.createValueGrip(this.obj.proto) };
},
}),
/**
* Handle a protocol request to provide the property descriptor of the
@ -1126,10 +1225,7 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onProperty: function OA_onProperty(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onProperty: PauseScopedActor.withPaused(function OA_onProperty(aRequest) {
if (!aRequest.name) {
return { error: "missingParameter",
message: "no property name was specified" };
@ -1138,7 +1234,7 @@ ObjectActor.prototype = {
let desc = this.obj.getOwnPropertyDescriptor(aRequest.name);
return { from: this.actorID,
descriptor: this._propertyDescriptor(desc) };
},
}),
/**
* A helper method that creates a property descriptor for the provided object,
@ -1167,11 +1263,7 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onDecompile: function OA_onDecompile(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onDecompile: PauseScopedActor.withPaused(function OA_onDecompile(aRequest) {
if (this.obj.class !== "Function") {
return { error: "objectNotFunction",
message: "decompile request is only valid for object grips " +
@ -1180,7 +1272,7 @@ ObjectActor.prototype = {
return { from: this.actorID,
decompiledCode: this.obj.decompile(!!aRequest.pretty) };
},
}),
/**
* Handle a protocol request to provide the lexical scope of a function.
@ -1188,11 +1280,7 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onScope: function OA_onScope(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onScope: PauseScopedActor.withPaused(function OA_onScope(aRequest) {
if (this.obj.class !== "Function") {
return { error: "objectNotFunction",
message: "scope request is only valid for object grips with a" +
@ -1212,7 +1300,7 @@ ObjectActor.prototype = {
// use the 'scope' request in the debugger frontend.
return { name: this.obj.name || null,
scope: envActor.form(this.obj) };
},
}),
/**
* Handle a protocol request to provide the name and parameters of a function.
@ -1220,11 +1308,7 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onNameAndParameters: function OA_onNameAndParameters(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onNameAndParameters: PauseScopedActor.withPaused(function OA_onNameAndParameters(aRequest) {
if (this.obj.class !== "Function") {
return { error: "objectNotFunction",
message: "nameAndParameters request is only valid for object " +
@ -1233,7 +1317,7 @@ ObjectActor.prototype = {
return { name: this.obj.name || null,
parameters: this.obj.parameterNames };
},
}),
/**
* Handle a protocol request to promote a pause-lifetime grip to a
@ -1242,13 +1326,9 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onThreadGrip: function OA_onThreadGrip(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onThreadGrip: PauseScopedActor.withPaused(function OA_onThreadGrip(aRequest) {
return { threadGrip: this.threadActor.threadObjectGrip(this.obj) };
},
}),
/**
* Handle a protocol request to release a thread-lifetime grip.
@ -1256,10 +1336,7 @@ ObjectActor.prototype = {
* @param aRequest object
* The protocol request object.
*/
onRelease: function OA_onRelease(aRequest) {
if (this.threadActor.state !== "paused") {
return this.WRONG_STATE_RESPONSE;
}
onRelease: PauseScopedActor.withPaused(function OA_onRelease(aRequest) {
if (this.registeredPool !== this.threadActor.threadLifetimePool) {
return { error: "notReleasable",
message: "only thread-lifetime actors can be released." };
@ -1267,8 +1344,8 @@ ObjectActor.prototype = {
this.release();
return {};
},
};
}),
});
ObjectActor.prototype.requestTypes = {
"nameAndParameters": ObjectActor.prototype.onNameAndParameters,
@ -1283,6 +1360,65 @@ ObjectActor.prototype.requestTypes = {
};
/**
* Creates an actor for the specied "very long" string. "Very long" is specified
* at the server's discretion.
*
* @param aString String
* The string.
*/
function LongStringActor(aString)
{
this.string = aString;
this.stringLength = aString.length;
}
LongStringActor.prototype = {
actorPrefix: "longString",
disconnect: function LSA_disconnect() {
// Because longStringActors is not a weak map, we won't automatically leave
// it so we need to manually leave on disconnect so that we don't leak
// memory.
if (this.registeredPool && this.registeredPool.longStringActors) {
delete this.registeredPool.longStringActors[this.actorID];
}
},
/**
* Returns a grip for this actor for returning in a protocol message.
*/
grip: function LSA_grip() {
return {
"type": "longString",
"initial": this.string.substring(
0, DebuggerServer.LONG_STRING_INITIAL_LENGTH),
"length": this.stringLength,
"actor": this.actorID
};
},
/**
* Handle a request to extract part of this actor's string.
*
* @param aRequest object
* The protocol request object.
*/
onSubstring: function LSA_onSubString(aRequest) {
return {
"from": this.actorID,
"substring": this.string.substring(aRequest.start, aRequest.end)
};
}
};
LongStringActor.prototype.requestTypes = {
"substring": LongStringActor.prototype.onSubstring
};
/**
* Creates an actor for the specified stack frame.
*

Просмотреть файл

@ -61,6 +61,9 @@ var DebuggerServer = {
xpcInspector: null,
_allowConnection: null,
LONG_STRING_LENGTH: 10000,
LONG_STRING_INITIAL_LENGTH: 1000,
/**
* Initialize the debugger server.
*

Просмотреть файл

@ -0,0 +1,105 @@
/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function run_test()
{
let loader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Components.interfaces.mozIJSSubScriptLoader);
loader.loadSubScript("chrome://global/content/devtools/dbg-script-actors.js");
test_LSA_disconnect();
test_LSA_grip();
test_LSA_onSubstring();
}
const TEST_STRING = "This is a very long string!";
function makeMockLongStringActor()
{
let string = TEST_STRING;
let actor = new LongStringActor(string);
actor.actorID = "longString1";
actor.registeredPool = {
longStringActors: {
longString1: actor
}
};
return actor;
}
function test_LSA_disconnect()
{
let actor = makeMockLongStringActor();
do_check_eq(actor.registeredPool.longStringActors[actor.actorID], actor);
actor.disconnect();
do_check_eq(actor.registeredPool.longStringActors[actor.actorID], void 0);
}
function test_LSA_substring()
{
let actor = makeMockLongStringActor();
do_check_eq(actor._substring(0, 4), TEST_STRING.substring(0, 4));
do_check_eq(actor._substring(6, 9), TEST_STRING.substring(6, 9));
do_check_eq(actor._substring(0, TEST_STRING.length), TEST_STRING);
}
function test_LSA_grip()
{
let actor = makeMockLongStringActor();
let grip = actor.grip();
do_check_eq(grip.type, "longString");
do_check_eq(grip.initial, TEST_STRING.substring(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH));
do_check_eq(grip.length, TEST_STRING.length);
do_check_eq(grip.actor, actor.actorID);
}
function test_LSA_onSubstring()
{
let actor = makeMockLongStringActor();
let response;
// From the start
response = actor.onSubstring({
start: 0,
end: 4
});
do_check_eq(response.from, actor.actorID);
do_check_eq(response.substring, TEST_STRING.substring(0, 4));
// In the middle
response = actor.onSubstring({
start: 5,
end: 8
});
do_check_eq(response.from, actor.actorID);
do_check_eq(response.substring, TEST_STRING.substring(5, 8));
// Whole string
response = actor.onSubstring({
start: 0,
end: TEST_STRING.length
});
do_check_eq(response.from, actor.actorID);
do_check_eq(response.substring, TEST_STRING);
// Negative index
response = actor.onSubstring({
start: -5,
end: TEST_STRING.length
});
do_check_eq(response.from, actor.actorID);
do_check_eq(response.substring,
TEST_STRING.substring(-5, TEST_STRING.length));
// Past the end
response = actor.onSubstring({
start: TEST_STRING.length - 5,
end: 100
});
do_check_eq(response.from, actor.actorID);
do_check_eq(response.substring,
TEST_STRING.substring(TEST_STRING.length - 5, 100));
}

Просмотреть файл

@ -0,0 +1,71 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
var gDebuggee;
var gClient;
var gThreadClient;
function run_test()
{
initTestDebuggerServer();
gDebuggee = addTestGlobal("test-grips");
gDebuggee.eval(function stopMe(arg1) {
debugger;
}.toString());
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestGlobalClientAndResume(gClient, "test-grips", function(aResponse, aThreadClient) {
gThreadClient = aThreadClient;
test_longstring_grip();
});
});
do_test_pending();
}
function test_longstring_grip()
{
let longString = "All I want is to be a monkey of moderate intelligence who"
+ " wears a suit... that's why I'm transferring to business school! Maybe I"
+ " love you so much, I love you no matter who you are pretending to be."
+ " Enough about your promiscuous mother, Hermes! We have bigger problems."
+ " For example, if you killed your grandfather, you'd cease to exist! What"
+ " kind of a father would I be if I said no? Yep, I remember. They came in"
+ " last at the Olympics, then retired to promote alcoholic beverages! And"
+ " remember, don't do anything that affects anything, unless it turns out"
+ " you were supposed to, in which case, for the love of God, don't not do"
+ " it!";
DebuggerServer.LONG_STRING_LENGTH = 200;
gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
let args = aPacket.frame.arguments;
do_check_eq(args.length, 1);
let grip = args[0];
try {
do_check_eq(grip.type, "longString");
do_check_eq(grip.length, longString.length);
do_check_eq(grip.initial, longString.substr(0, DebuggerServer.LONG_STRING_INITIAL_LENGTH));
let longStringClient = gThreadClient.longString(grip);
longStringClient.substring(22, 28, function (aResponse) {
try {
do_check_eq(aResponse.substring, "monkey");
} finally {
gThreadClient.resume(function() {
finishClient(gClient);
});
}
});
} catch(error) {
gThreadClient.resume(function() {
finishClient(gClient);
do_throw(error);
});
}
});
gDebuggee.eval('stopMe("' + longString + '")');
}

Просмотреть файл

@ -0,0 +1,60 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
var gDebuggee;
var gClient;
var gThreadClient;
function run_test()
{
initTestDebuggerServer();
gDebuggee = addTestGlobal("test-grips");
gDebuggee.eval(function stopMe(arg1) {
debugger;
}.toString());
gClient = new DebuggerClient(DebuggerServer.connectPipe());
gClient.connect(function() {
attachTestGlobalClientAndResume(
gClient, "test-grips", function(aResponse, aThreadClient) {
gThreadClient = aThreadClient;
test_longstring_grip();
});
});
do_test_pending();
}
function test_longstring_grip()
{
DebuggerServer.LONG_STRING_LENGTH = 200;
gThreadClient.addOneTimeListener("paused", function(aEvent, aPacket) {
try {
let fakeLongStringGrip = {
type: "longString",
length: 1000000,
actor: "123fakeActor123",
initial: ""
};
let longStringClient = gThreadClient.longString(fakeLongStringGrip);
longStringClient.substring(22, 28, function (aResponse) {
try {
do_check_true(!!aResponse.error,
"We should not get a response, but an error.");
} finally {
gThreadClient.resume(function() {
finishClient(gClient);
});
}
});
} catch(error) {
gThreadClient.resume(function() {
finishClient(gClient);
do_throw(error);
});
}
});
gDebuggee.eval('stopMe()');
}

Просмотреть файл

@ -60,4 +60,7 @@ tail =
[test_framebindings-05.js]
[test_pause_exceptions-01.js]
[test_pause_exceptions-02.js]
[test_longstringactor.js]
[test_longstringgrips-01.js]
[test_longstringgrips-02.js]
[test_breakpointstore.js]