Bug 1177279 - Create a SourceLocationController to manage the state of updating sources for source mapping. r=jlong,jryans
This commit is contained in:
@@ -16,6 +16,7 @@ DevToolsModules(
|
|||||||
'gDevTools.jsm',
|
'gDevTools.jsm',
|
||||||
'selection.js',
|
'selection.js',
|
||||||
'sidebar.js',
|
'sidebar.js',
|
||||||
|
'source-location.js',
|
||||||
'target.js',
|
'target.js',
|
||||||
'toolbox-highlighter-utils.js',
|
'toolbox-highlighter-utils.js',
|
||||||
'toolbox-hosts.js',
|
'toolbox-hosts.js',
|
||||||
|
|||||||
137
devtools/client/framework/source-location.js
Normal file
137
devtools/client/framework/source-location.js
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const { Task } = require("resource://gre/modules/Task.jsm");
|
||||||
|
const { assert } = require("devtools/shared/DevToolsUtils");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A manager class that wraps a TabTarget and listens to source changes
|
||||||
|
* from source maps and resolves non-source mapped locations to the source mapped
|
||||||
|
* versions and back and forth, and creating smart elements with a location that
|
||||||
|
* auto-update when the source changes (from pretty printing, source maps loading, etc)
|
||||||
|
*
|
||||||
|
* @param {TabTarget} target
|
||||||
|
*/
|
||||||
|
function SourceLocationController(target) {
|
||||||
|
this.target = target;
|
||||||
|
this.locations = new Set();
|
||||||
|
|
||||||
|
this._onSourceUpdated = this._onSourceUpdated.bind(this);
|
||||||
|
this.reset = this.reset.bind(this);
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
|
|
||||||
|
target.on("source-updated", this._onSourceUpdated);
|
||||||
|
target.on("navigate", this.reset);
|
||||||
|
target.on("will-navigate", this.reset);
|
||||||
|
target.on("close", this.destroy);
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceLocationController.prototype.reset = function() {
|
||||||
|
this.locations.clear();
|
||||||
|
};
|
||||||
|
|
||||||
|
SourceLocationController.prototype.destroy = function() {
|
||||||
|
this.locations.clear();
|
||||||
|
this.target.off("source-updated", this._onSourceUpdated);
|
||||||
|
this.target.off("navigate", this.reset);
|
||||||
|
this.target.off("will-navigate", this.reset);
|
||||||
|
this.target.off("close", this.destroy);
|
||||||
|
this.target = this.locations = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add this `location` to be observed and register a callback
|
||||||
|
* whenever the underlying source is updated.
|
||||||
|
*
|
||||||
|
* @param {Object} location
|
||||||
|
* An object with a {String} url, {Number} line, and optionally
|
||||||
|
* a {Number} column.
|
||||||
|
* @param {Function} callback
|
||||||
|
*/
|
||||||
|
SourceLocationController.prototype.bindLocation = function(location, callback) {
|
||||||
|
assert(location.url, "Location must have a url.");
|
||||||
|
assert(location.line, "Location must have a line.");
|
||||||
|
this.locations.add({ location, callback });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a new source occurs (a normal source, source maps) or an updated
|
||||||
|
* source (pretty print) occurs.
|
||||||
|
*
|
||||||
|
* @param {String} eventName
|
||||||
|
* @param {Object} sourceEvent
|
||||||
|
*/
|
||||||
|
SourceLocationController.prototype._onSourceUpdated = function(_, sourceEvent) {
|
||||||
|
let { type, source } = sourceEvent;
|
||||||
|
// If we get a new source, and it's not a source map, abort;
|
||||||
|
// we can ahve no actionable updates as this is just a new normal source.
|
||||||
|
// Also abort if there's no `url`, which means it's unsourcemappable anyway,
|
||||||
|
// like an eval script.
|
||||||
|
if (!source.url || type === "newSource" && !source.isSourceMapped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let locationItem of this.locations) {
|
||||||
|
if (isSourceRelated(locationItem.location, source)) {
|
||||||
|
this._updateSource(locationItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
SourceLocationController.prototype._updateSource = Task.async(function*(locationItem) {
|
||||||
|
let newLocation = yield resolveLocation(this.target, locationItem.location);
|
||||||
|
if (newLocation) {
|
||||||
|
let previousLocation = Object.assign({}, locationItem.location);
|
||||||
|
Object.assign(locationItem.location, newLocation);
|
||||||
|
locationItem.callback(previousLocation, newLocation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Take a TabTarget and a location, containing a `url`, `line`, and `column`, resolve
|
||||||
|
* the location to the latest location (so a source mapped location, or if pretty print
|
||||||
|
* status has been updated)
|
||||||
|
*
|
||||||
|
* @param {TabTarget} target
|
||||||
|
* @param {Object} location
|
||||||
|
* @return {Promise<Object>}
|
||||||
|
*/
|
||||||
|
function resolveLocation(target, location) {
|
||||||
|
return Task.spawn(function*() {
|
||||||
|
let newLocation = yield target.resolveLocation({
|
||||||
|
url: location.url,
|
||||||
|
line: location.line,
|
||||||
|
column: location.column || Infinity
|
||||||
|
});
|
||||||
|
|
||||||
|
// Source or mapping not found, so don't do anything
|
||||||
|
if (newLocation.error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return newLocation;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a serialized SourceActor form and returns a boolean indicating
|
||||||
|
* if this source is related to this location, like if a location is a generated source,
|
||||||
|
* and the source map is loaded subsequently, the new source mapped SourceActor
|
||||||
|
* will be considered related to this location. Same with pretty printing new sources.
|
||||||
|
*
|
||||||
|
* @param {Object} location
|
||||||
|
* @param {Object} source
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
function isSourceRelated(location, source) {
|
||||||
|
// Mapping location to subsequently loaded source map
|
||||||
|
return source.generatedUrl === location.url ||
|
||||||
|
// Mapping source map loc to source map
|
||||||
|
source.url === location.url
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.SourceLocationController = SourceLocationController;
|
||||||
|
exports.resolveLocation = resolveLocation;
|
||||||
|
exports.isSourceRelated = isSourceRelated;
|
||||||
@@ -397,6 +397,7 @@ TabTarget.prototype = {
|
|||||||
}
|
}
|
||||||
this.activeTab = tabClient;
|
this.activeTab = tabClient;
|
||||||
this.threadActor = response.threadActor;
|
this.threadActor = response.threadActor;
|
||||||
|
|
||||||
attachConsole();
|
attachConsole();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -498,6 +499,10 @@ TabTarget.prototype = {
|
|||||||
this.emit("frame-update", aPacket);
|
this.emit("frame-update", aPacket);
|
||||||
};
|
};
|
||||||
this.client.addListener("frameUpdate", this._onFrameUpdate);
|
this.client.addListener("frameUpdate", this._onFrameUpdate);
|
||||||
|
|
||||||
|
this._onSourceUpdated = (event, packet) => this.emit("source-updated", packet);
|
||||||
|
this.client.addListener("newSource", this._onSourceUpdated);
|
||||||
|
this.client.addListener("updatedSource", this._onSourceUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -508,6 +513,8 @@ TabTarget.prototype = {
|
|||||||
this.client.removeListener("tabNavigated", this._onTabNavigated);
|
this.client.removeListener("tabNavigated", this._onTabNavigated);
|
||||||
this.client.removeListener("tabDetached", this._onTabDetached);
|
this.client.removeListener("tabDetached", this._onTabDetached);
|
||||||
this.client.removeListener("frameUpdate", this._onFrameUpdate);
|
this.client.removeListener("frameUpdate", this._onFrameUpdate);
|
||||||
|
this.client.removeListener("newSource", this._onSourceUpdated);
|
||||||
|
this.client.removeListener("updatedSource", this._onSourceUpdated);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,6 +610,20 @@ TabTarget.prototype = {
|
|||||||
let id = this._tab ? this._tab : (this._form && this._form.actor);
|
let id = this._tab ? this._tab : (this._form && this._form.actor);
|
||||||
return `TabTarget:${id}`;
|
return `TabTarget:${id}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see TabActor.prototype.onResolveLocation
|
||||||
|
*/
|
||||||
|
resolveLocation(loc) {
|
||||||
|
let deferred = promise.defer();
|
||||||
|
|
||||||
|
this.client.request(Object.assign({
|
||||||
|
to: this._form.actor,
|
||||||
|
type: "resolveLocation",
|
||||||
|
}, loc), deferred.resolve);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ support-files =
|
|||||||
browser_toolbox_options_disable_cache.sjs
|
browser_toolbox_options_disable_cache.sjs
|
||||||
browser_toolbox_sidebar_tool.xul
|
browser_toolbox_sidebar_tool.xul
|
||||||
code_math.js
|
code_math.js
|
||||||
|
code_ugly.js
|
||||||
head.js
|
head.js
|
||||||
shared-head.js
|
shared-head.js
|
||||||
shared-redux-head.js
|
shared-redux-head.js
|
||||||
@@ -26,6 +27,8 @@ support-files =
|
|||||||
[browser_keybindings_02.js]
|
[browser_keybindings_02.js]
|
||||||
[browser_keybindings_03.js]
|
[browser_keybindings_03.js]
|
||||||
[browser_new_activation_workflow.js]
|
[browser_new_activation_workflow.js]
|
||||||
|
[browser_source-location-01.js]
|
||||||
|
[browser_source-location-02.js]
|
||||||
[browser_target_events.js]
|
[browser_target_events.js]
|
||||||
[browser_target_remote.js]
|
[browser_target_remote.js]
|
||||||
[browser_target_support.js]
|
[browser_target_support.js]
|
||||||
|
|||||||
96
devtools/client/framework/test/browser_source-location-01.js
Normal file
96
devtools/client/framework/test/browser_source-location-01.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the SourceMapController updates generated sources when source maps
|
||||||
|
* are subsequently found. Also checks when no column is provided, and
|
||||||
|
* when tagging an already source mapped location initially.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
|
||||||
|
// Empty page
|
||||||
|
const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
|
||||||
|
const JS_URL = `${DEBUGGER_ROOT}code_binary_search.js`;
|
||||||
|
const COFFEE_URL = `${DEBUGGER_ROOT}code_binary_search.coffee`;
|
||||||
|
const { SourceLocationController } = require("devtools/client/framework/source-location");
|
||||||
|
|
||||||
|
add_task(function*() {
|
||||||
|
let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
|
||||||
|
|
||||||
|
let controller = new SourceLocationController(toolbox.target);
|
||||||
|
|
||||||
|
let aggregator = [];
|
||||||
|
|
||||||
|
function onUpdate (oldLoc, newLoc) {
|
||||||
|
if (oldLoc.line === 6) {
|
||||||
|
checkLoc1(oldLoc, newLoc);
|
||||||
|
} else if (oldLoc.line === 8) {
|
||||||
|
checkLoc2(oldLoc, newLoc);
|
||||||
|
} else if (oldLoc.line === 2) {
|
||||||
|
checkLoc3(oldLoc, newLoc);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
|
||||||
|
}
|
||||||
|
aggregator.push(newLoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
let loc1 = { url: JS_URL, line: 6 };
|
||||||
|
let loc2 = { url: JS_URL, line: 8, column: 3 };
|
||||||
|
let loc3 = { url: COFFEE_URL, line: 2, column: 0 };
|
||||||
|
|
||||||
|
controller.bindLocation(loc1, onUpdate);
|
||||||
|
controller.bindLocation(loc2, onUpdate);
|
||||||
|
controller.bindLocation(loc3, onUpdate);
|
||||||
|
|
||||||
|
// Inject JS script
|
||||||
|
yield createScript(JS_URL);
|
||||||
|
|
||||||
|
yield waitUntil(() => aggregator.length === 3);
|
||||||
|
|
||||||
|
ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 4), "found first updated location");
|
||||||
|
ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 6), "found second updated location");
|
||||||
|
ok(aggregator.find(i => i.url === COFFEE_URL && i.line === 2), "found third updated location");
|
||||||
|
|
||||||
|
yield toolbox.destroy();
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkLoc1 (oldLoc, newLoc) {
|
||||||
|
is(oldLoc.line, 6, "Correct line for JS:6");
|
||||||
|
is(oldLoc.column, null, "Correct column for JS:6");
|
||||||
|
is(oldLoc.url, JS_URL, "Correct url for JS:6");
|
||||||
|
is(newLoc.line, 4, "Correct line for JS:6 -> COFFEE");
|
||||||
|
is(newLoc.column, 2, "Correct column for JS:6 -> COFFEE -- handles falsy column entries");
|
||||||
|
is(newLoc.url, COFFEE_URL, "Correct url for JS:6 -> COFFEE");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkLoc2 (oldLoc, newLoc) {
|
||||||
|
is(oldLoc.line, 8, "Correct line for JS:8:3");
|
||||||
|
is(oldLoc.column, 3, "Correct column for JS:8:3");
|
||||||
|
is(oldLoc.url, JS_URL, "Correct url for JS:8:3");
|
||||||
|
is(newLoc.line, 6, "Correct line for JS:8:3 -> COFFEE");
|
||||||
|
is(newLoc.column, 10, "Correct column for JS:8:3 -> COFFEE");
|
||||||
|
is(newLoc.url, COFFEE_URL, "Correct url for JS:8:3 -> COFFEE");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkLoc3 (oldLoc, newLoc) {
|
||||||
|
is(oldLoc.line, 2, "Correct line for COFFEE:2:0");
|
||||||
|
is(oldLoc.column, 0, "Correct column for COFFEE:2:0");
|
||||||
|
is(oldLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0");
|
||||||
|
is(newLoc.line, 2, "Correct line for COFFEE:2:0 -> COFFEE");
|
||||||
|
is(newLoc.column, 0, "Correct column for COFFEE:2:0 -> COFFEE");
|
||||||
|
is(newLoc.url, COFFEE_URL, "Correct url for COFFEE:2:0 -> COFFEE");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScript (url) {
|
||||||
|
info(`Creating script: ${url}`);
|
||||||
|
let mm = getFrameScript();
|
||||||
|
let command = `
|
||||||
|
let script = document.createElement("script");
|
||||||
|
script.setAttribute("src", "${url}");
|
||||||
|
document.body.appendChild(script);
|
||||||
|
null;
|
||||||
|
`;
|
||||||
|
return evalInDebuggee(mm, command);
|
||||||
|
}
|
||||||
107
devtools/client/framework/test/browser_source-location-02.js
Normal file
107
devtools/client/framework/test/browser_source-location-02.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/* vim: set ts=2 et sw=2 tw=80: */
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the SourceLocationController updates generated sources when pretty printing
|
||||||
|
* and un pretty printing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DEBUGGER_ROOT = "http://example.com/browser/devtools/client/debugger/test/mochitest/";
|
||||||
|
// Empty page
|
||||||
|
const PAGE_URL = `${DEBUGGER_ROOT}doc_empty-tab-01.html`;
|
||||||
|
const JS_URL = `${URL_ROOT}code_ugly.js`;
|
||||||
|
const { SourceLocationController } = require("devtools/client/framework/source-location");
|
||||||
|
|
||||||
|
add_task(function*() {
|
||||||
|
let toolbox = yield openNewTabAndToolbox(PAGE_URL, "jsdebugger");
|
||||||
|
|
||||||
|
let controller = new SourceLocationController(toolbox.target);
|
||||||
|
|
||||||
|
let checkedPretty = false;
|
||||||
|
let checkedUnpretty = false;
|
||||||
|
|
||||||
|
function onUpdate (oldLoc, newLoc) {
|
||||||
|
if (oldLoc.line === 3) {
|
||||||
|
checkPrettified(oldLoc, newLoc);
|
||||||
|
checkedPretty = true;
|
||||||
|
} else if (oldLoc.line === 9) {
|
||||||
|
checkUnprettified(oldLoc, newLoc);
|
||||||
|
checkedUnpretty = true;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unexpected location update: ${JSON.stringify(oldLoc)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.bindLocation({ url: JS_URL, line: 3 }, onUpdate);
|
||||||
|
|
||||||
|
// Inject JS script
|
||||||
|
let sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
|
||||||
|
yield createScript(JS_URL);
|
||||||
|
yield sourceShown;
|
||||||
|
|
||||||
|
let ppButton = toolbox.getCurrentPanel().panelWin.document.getElementById("pretty-print");
|
||||||
|
sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
|
||||||
|
ppButton.click();
|
||||||
|
yield sourceShown;
|
||||||
|
yield waitUntil(() => checkedPretty);
|
||||||
|
|
||||||
|
// TODO check unprettified change once bug 1177446 fixed
|
||||||
|
/*
|
||||||
|
sourceShown = waitForSourceShown(toolbox.getCurrentPanel(), "code_ugly.js");
|
||||||
|
ppButton.click();
|
||||||
|
yield sourceShown;
|
||||||
|
yield waitUntil(() => checkedUnpretty);
|
||||||
|
*/
|
||||||
|
|
||||||
|
yield toolbox.destroy();
|
||||||
|
gBrowser.removeCurrentTab();
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkPrettified (oldLoc, newLoc) {
|
||||||
|
is(oldLoc.line, 3, "Correct line for JS:3");
|
||||||
|
is(oldLoc.column, null, "Correct column for JS:3");
|
||||||
|
is(oldLoc.url, JS_URL, "Correct url for JS:3");
|
||||||
|
is(newLoc.line, 9, "Correct line for JS:3 -> PRETTY");
|
||||||
|
is(newLoc.column, 0, "Correct column for JS:3 -> PRETTY");
|
||||||
|
is(newLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkUnprettified (oldLoc, newLoc) {
|
||||||
|
is(oldLoc.line, 9, "Correct line for JS:3 -> PRETTY");
|
||||||
|
is(oldLoc.column, 0, "Correct column for JS:3 -> PRETTY");
|
||||||
|
is(oldLoc.url, JS_URL, "Correct url for JS:3 -> PRETTY");
|
||||||
|
is(newLoc.line, 3, "Correct line for JS:3 -> UNPRETTIED");
|
||||||
|
is(newLoc.column, null, "Correct column for JS:3 -> UNPRETTIED");
|
||||||
|
is(newLoc.url, JS_URL, "Correct url for JS:3 -> UNPRETTIED");
|
||||||
|
}
|
||||||
|
|
||||||
|
function createScript (url) {
|
||||||
|
info(`Creating script: ${url}`);
|
||||||
|
let mm = getFrameScript();
|
||||||
|
let command = `
|
||||||
|
let script = document.createElement("script");
|
||||||
|
script.setAttribute("src", "${url}");
|
||||||
|
document.body.appendChild(script);
|
||||||
|
`;
|
||||||
|
return evalInDebuggee(mm, command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForSourceShown (debuggerPanel, url) {
|
||||||
|
let { panelWin } = debuggerPanel;
|
||||||
|
let deferred = promise.defer();
|
||||||
|
|
||||||
|
info(`Waiting for source ${url} to be shown in the debugger...`);
|
||||||
|
panelWin.on(panelWin.EVENTS.SOURCE_SHOWN, function onSourceShown (_, source) {
|
||||||
|
let sourceUrl = source.url || source.introductionUrl;
|
||||||
|
|
||||||
|
if (sourceUrl.includes(url)) {
|
||||||
|
panelWin.off(panelWin.EVENTS.SOURCE_SHOWN, onSourceShown);
|
||||||
|
info(`Source shown for ${url}`);
|
||||||
|
deferred.resolve(source);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
3
devtools/client/framework/test/code_ugly.js
Normal file
3
devtools/client/framework/test/code_ugly.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
function foo() { var a=1; var b=2; bar(a, b); }
|
||||||
|
function bar(c, d) { return c - d; }
|
||||||
|
foo();
|
||||||
@@ -218,6 +218,7 @@ var openToolboxForTab = Task.async(function*(tab, toolId, hostType) {
|
|||||||
|
|
||||||
let toolbox;
|
let toolbox;
|
||||||
let target = TargetFactory.forTab(tab);
|
let target = TargetFactory.forTab(tab);
|
||||||
|
yield target.makeRemote();
|
||||||
|
|
||||||
// Check if the toolbox is already loaded.
|
// Check if the toolbox is already loaded.
|
||||||
toolbox = gDevTools.getToolbox(target);
|
toolbox = gDevTools.getToolbox(target);
|
||||||
@@ -263,3 +264,45 @@ function closeToolboxAndTab(toolbox) {
|
|||||||
gBrowser.removeCurrentTab();
|
gBrowser.removeCurrentTab();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until a predicate returns true.
|
||||||
|
*
|
||||||
|
* @param function predicate
|
||||||
|
* Invoked once in a while until it returns true.
|
||||||
|
* @param number interval [optional]
|
||||||
|
* How often the predicate is invoked, in milliseconds.
|
||||||
|
*/
|
||||||
|
function waitUntil(predicate, interval = 10) {
|
||||||
|
if (predicate()) {
|
||||||
|
return Promise.resolve(true);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(function() {
|
||||||
|
waitUntil(predicate, interval).then(() => resolve(true));
|
||||||
|
}, interval);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a string `script` and evaluates it directly in the content
|
||||||
|
* in potentially a different process.
|
||||||
|
*/
|
||||||
|
let MM_INC_ID = 0;
|
||||||
|
function evalInDebuggee (mm, script) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
let id = MM_INC_ID++;
|
||||||
|
mm.sendAsyncMessage("devtools:test:eval", { script, id });
|
||||||
|
mm.addMessageListener("devtools:test:eval:response", handler);
|
||||||
|
|
||||||
|
function handler ({ data }) {
|
||||||
|
if (id !== data.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
info(`Successfully evaled in debuggee: ${script}`);
|
||||||
|
mm.removeMessageListener("devtools:test:eval:response", handler);
|
||||||
|
resolve(data.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -124,8 +124,7 @@ addMessageListener("devtools:test:profiler", function ({ data: { method, args, i
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// To eval in content, look at `evalInDebuggee` in the head.js of canvasdebugger
|
// To eval in content, look at `evalInDebuggee` in the shared-head.js.
|
||||||
// for an example.
|
|
||||||
addMessageListener("devtools:test:eval", function ({ data }) {
|
addMessageListener("devtools:test:eval", function ({ data }) {
|
||||||
sendAsyncMessage("devtools:test:eval:response", {
|
sendAsyncMessage("devtools:test:eval:response", {
|
||||||
value: content.eval(data.script),
|
value: content.eval(data.script),
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ function ThreadActor(aParent, aGlobal)
|
|||||||
|
|
||||||
this._allEventsListener = this._allEventsListener.bind(this);
|
this._allEventsListener = this._allEventsListener.bind(this);
|
||||||
this.onNewGlobal = this.onNewGlobal.bind(this);
|
this.onNewGlobal = this.onNewGlobal.bind(this);
|
||||||
this.onNewSource = this.onNewSource.bind(this);
|
this.onSourceEvent = this.onSourceEvent.bind(this);
|
||||||
this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
|
this.uncaughtExceptionHook = this.uncaughtExceptionHook.bind(this);
|
||||||
this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
|
this.onDebuggerStatement = this.onDebuggerStatement.bind(this);
|
||||||
this.onNewScript = this.onNewScript.bind(this);
|
this.onNewScript = this.onNewScript.bind(this);
|
||||||
@@ -583,6 +583,8 @@ ThreadActor.prototype = {
|
|||||||
this._sourceActorStore = null;
|
this._sourceActorStore = null;
|
||||||
|
|
||||||
events.off(this._parent, "window-ready", this._onWindowReady);
|
events.off(this._parent, "window-ready", this._onWindowReady);
|
||||||
|
this.sources.off("newSource", this.onSourceEvent);
|
||||||
|
this.sources.off("updatedSource", this.onSourceEvent);
|
||||||
this.clearDebuggees();
|
this.clearDebuggees();
|
||||||
this.conn.removeActorPool(this._threadLifetimePool);
|
this.conn.removeActorPool(this._threadLifetimePool);
|
||||||
this._threadLifetimePool = null;
|
this._threadLifetimePool = null;
|
||||||
@@ -623,9 +625,8 @@ ThreadActor.prototype = {
|
|||||||
|
|
||||||
update(this._options, aRequest.options || {});
|
update(this._options, aRequest.options || {});
|
||||||
this.sources.setOptions(this._options);
|
this.sources.setOptions(this._options);
|
||||||
this.sources.on('newSource', (name, source) => {
|
this.sources.on("newSource", this.onSourceEvent);
|
||||||
this.onNewSource(source);
|
this.sources.on("updatedSource", this.onSourceEvent);
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize an event loop stack. This can't be done in the constructor,
|
// Initialize an event loop stack. This can't be done in the constructor,
|
||||||
// because this.conn is not yet initialized by the actor pool at that time.
|
// because this.conn is not yet initialized by the actor pool at that time.
|
||||||
@@ -1894,12 +1895,29 @@ ThreadActor.prototype = {
|
|||||||
this._addSource(aScript.source);
|
this._addSource(aScript.source);
|
||||||
},
|
},
|
||||||
|
|
||||||
onNewSource: function (aSource) {
|
/**
|
||||||
|
* A function called when there's a new or updated source from a thread actor's
|
||||||
|
* sources. Emits `newSource` and `updatedSource` on the tab actor.
|
||||||
|
*
|
||||||
|
* @param {String} name
|
||||||
|
* @param {SourceActor} source
|
||||||
|
*/
|
||||||
|
onSourceEvent: function (name, source) {
|
||||||
this.conn.send({
|
this.conn.send({
|
||||||
from: this.actorID,
|
from: this._parent.actorID,
|
||||||
type: "newSource",
|
type: name,
|
||||||
source: aSource.form()
|
source: source.form()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// For compatibility and debugger still using `newSource` on the thread client,
|
||||||
|
// still emit this event here. Clean up in bug 1247084
|
||||||
|
if (name === "newSource") {
|
||||||
|
this.conn.send({
|
||||||
|
from: this.actorID,
|
||||||
|
type: name,
|
||||||
|
source: source.form()
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2024,7 +2042,7 @@ ThreadActor.prototype.requestTypes = {
|
|||||||
"releaseMany": ThreadActor.prototype.onReleaseMany,
|
"releaseMany": ThreadActor.prototype.onReleaseMany,
|
||||||
"sources": ThreadActor.prototype.onSources,
|
"sources": ThreadActor.prototype.onSources,
|
||||||
"threadGrips": ThreadActor.prototype.onThreadGrips,
|
"threadGrips": ThreadActor.prototype.onThreadGrips,
|
||||||
"prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties
|
"prototypesAndProperties": ThreadActor.prototype.onPrototypesAndProperties,
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.ThreadActor = ThreadActor;
|
exports.ThreadActor = ThreadActor;
|
||||||
|
|||||||
@@ -169,10 +169,10 @@ let SourceActor = ActorClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
get isSourceMapped() {
|
get isSourceMapped() {
|
||||||
return !this.isInlineSource && (
|
return !!(!this.isInlineSource && (
|
||||||
this._originalURL || this._generatedSource ||
|
this._originalURL || this._generatedSource ||
|
||||||
this.threadActor.sources.isPrettyPrinted(this.url)
|
this.threadActor.sources.isPrettyPrinted(this.url)
|
||||||
);
|
));
|
||||||
},
|
},
|
||||||
|
|
||||||
get isInlineSource() {
|
get isInlineSource() {
|
||||||
@@ -211,11 +211,13 @@ let SourceActor = ActorClass({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
actor: this.actorID,
|
actor: this.actorID,
|
||||||
|
generatedUrl: this.generatedSource ? this.generatedSource.url : null,
|
||||||
url: this.url ? this.url.split(" -> ").pop() : null,
|
url: this.url ? this.url.split(" -> ").pop() : null,
|
||||||
addonID: this._addonID,
|
addonID: this._addonID,
|
||||||
addonPath: this._addonPath,
|
addonPath: this._addonPath,
|
||||||
isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
|
isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
|
||||||
isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url),
|
isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url),
|
||||||
|
isSourceMapped: this.isSourceMapped,
|
||||||
introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null,
|
introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null,
|
||||||
introductionType: source ? source.introductionType : null
|
introductionType: source ? source.introductionType : null
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ TabSources.prototype = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('getSourceByURL: could not find source for ' + url);
|
throw new Error('getSourceByURL: could not find source for ' + url);
|
||||||
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -557,6 +558,7 @@ TabSources.prototype = {
|
|||||||
// Forcefully set the sourcemap cache. This will be used even if
|
// Forcefully set the sourcemap cache. This will be used even if
|
||||||
// sourcemaps are disabled.
|
// sourcemaps are disabled.
|
||||||
this._sourceMapCache[url] = resolve(aMap);
|
this._sourceMapCache[url] = resolve(aMap);
|
||||||
|
this.emit("updatedSource", this.getSourceActor(aSource));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
var { Ci, Cu } = require("chrome");
|
var { Ci, Cu } = require("chrome");
|
||||||
var Services = require("Services");
|
var Services = require("Services");
|
||||||
var promise = require("promise");
|
var promise = require("promise");
|
||||||
var { ActorPool, createExtraActors, appendExtraActors } = require("devtools/server/actors/common");
|
var {
|
||||||
|
ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
|
||||||
|
} = require("devtools/server/actors/common");
|
||||||
var { DebuggerServer } = require("devtools/server/main");
|
var { DebuggerServer } = require("devtools/server/main");
|
||||||
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||||
var { assert } = DevToolsUtils;
|
var { assert } = DevToolsUtils;
|
||||||
@@ -1903,6 +1905,43 @@ TabActor.prototype = {
|
|||||||
delete this._extraActors[aName];
|
delete this._extraActors[aName];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a packet containing a url, line and column and returns
|
||||||
|
* the updated url, line and column based on the current source mapping
|
||||||
|
* (source mapped files, pretty prints).
|
||||||
|
*
|
||||||
|
* @param {String} request.url
|
||||||
|
* @param {Number} request.line
|
||||||
|
* @param {Number?} request.column
|
||||||
|
* @return {Promise<Object>}
|
||||||
|
*/
|
||||||
|
onResolveLocation: function (request) {
|
||||||
|
let { url, line } = request;
|
||||||
|
let column = request.column || 0;
|
||||||
|
let actor;
|
||||||
|
|
||||||
|
if (actor = this.sources.getSourceActorByURL(url)) {
|
||||||
|
// Get the generated source actor if this is source mapped
|
||||||
|
let generatedActor = actor.generatedSource ?
|
||||||
|
this.sources.createNonSourceMappedActor(actor.generatedSource) :
|
||||||
|
actor;
|
||||||
|
let generatedLocation = new GeneratedLocation(generatedActor, line, column);
|
||||||
|
|
||||||
|
return this.sources.getOriginalLocation(generatedLocation).then(loc => {
|
||||||
|
// If no map found, return this packet
|
||||||
|
if (loc.originalLine == null) {
|
||||||
|
return { from: this.actorID, type: "resolveLocation", error: "MAP_NOT_FOUND" };
|
||||||
|
}
|
||||||
|
|
||||||
|
loc = loc.toJSON();
|
||||||
|
return { from: this.actorID, url: loc.source.url, column: loc.column, line: loc.line };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to this packet when source is not found
|
||||||
|
return promise.resolve({ from: this.actorID, type: "resolveLocation", error: "SOURCE_NOT_FOUND" });
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1917,7 +1956,8 @@ TabActor.prototype.requestTypes = {
|
|||||||
"reconfigure": TabActor.prototype.onReconfigure,
|
"reconfigure": TabActor.prototype.onReconfigure,
|
||||||
"switchToFrame": TabActor.prototype.onSwitchToFrame,
|
"switchToFrame": TabActor.prototype.onSwitchToFrame,
|
||||||
"listFrames": TabActor.prototype.onListFrames,
|
"listFrames": TabActor.prototype.onListFrames,
|
||||||
"listWorkers": TabActor.prototype.onListWorkers
|
"listWorkers": TabActor.prototype.onListWorkers,
|
||||||
|
"resolveLocation": TabActor.prototype.onResolveLocation
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.TabActor = TabActor;
|
exports.TabActor = TabActor;
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ const UnsolicitedNotifications = {
|
|||||||
"appInstall": "appInstall",
|
"appInstall": "appInstall",
|
||||||
"appUninstall": "appUninstall",
|
"appUninstall": "appUninstall",
|
||||||
"evaluationResult": "evaluationResult",
|
"evaluationResult": "evaluationResult",
|
||||||
|
"newSource": "newSource",
|
||||||
|
"updatedSource": "updatedSource",
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -247,8 +249,8 @@ const DebuggerClient = exports.DebuggerClient = function (aTransport)
|
|||||||
* The `Request` object that is a Promise object and resolves once
|
* The `Request` object that is a Promise object and resolves once
|
||||||
* we receive the response. (See request method for more details)
|
* we receive the response. (See request method for more details)
|
||||||
*/
|
*/
|
||||||
DebuggerClient.requester = function (aPacketSkeleton,
|
DebuggerClient.requester = function (aPacketSkeleton, config={}) {
|
||||||
{ telemetry, before, after }) {
|
let { telemetry, before, after } = config;
|
||||||
return DevToolsUtils.makeInfallible(function (...args) {
|
return DevToolsUtils.makeInfallible(function (...args) {
|
||||||
let histogram, startTime;
|
let histogram, startTime;
|
||||||
if (telemetry) {
|
if (telemetry) {
|
||||||
@@ -1375,7 +1377,20 @@ TabClient.prototype = {
|
|||||||
|
|
||||||
attachWorker: function (aWorkerActor, aOnResponse) {
|
attachWorker: function (aWorkerActor, aOnResponse) {
|
||||||
this.client.attachWorker(aWorkerActor, aOnResponse);
|
this.client.attachWorker(aWorkerActor, aOnResponse);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a location ({ url, line, column }) to its current
|
||||||
|
* source mapping location.
|
||||||
|
*
|
||||||
|
* @param {String} arg[0].url
|
||||||
|
* @param {Number} arg[0].line
|
||||||
|
* @param {Number?} arg[0].column
|
||||||
|
*/
|
||||||
|
resolveLocation: DebuggerClient.requester({
|
||||||
|
type: "resolveLocation",
|
||||||
|
location: args(0)
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
eventSource(TabClient.prototype);
|
eventSource(TabClient.prototype);
|
||||||
|
|||||||
Reference in New Issue
Block a user