summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/source.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/server/actors/source.js')
-rw-r--r--devtools/server/actors/source.js902
1 files changed, 902 insertions, 0 deletions
diff --git a/devtools/server/actors/source.js b/devtools/server/actors/source.js
new file mode 100644
index 000000000..e76c14fe8
--- /dev/null
+++ b/devtools/server/actors/source.js
@@ -0,0 +1,902 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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 { Cc, Ci } = require("chrome");
+const Services = require("Services");
+const { BreakpointActor, setBreakpointAtEntryPoints } = require("devtools/server/actors/breakpoint");
+const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
+const { createValueGrip } = require("devtools/server/actors/object");
+const { ActorClassWithSpec, Arg, RetVal, method } = require("devtools/shared/protocol");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const { assert, fetch } = DevToolsUtils;
+const { joinURI } = require("devtools/shared/path");
+const promise = require("promise");
+const { defer, resolve, reject, all } = promise;
+const { sourceSpec } = require("devtools/shared/specs/source");
+
+loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
+loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
+loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
+
+function isEvalSource(source) {
+ let introType = source.introductionType;
+ // These are all the sources that are essentially eval-ed (either
+ // by calling eval or passing a string to one of these functions).
+ return (introType === "eval" ||
+ introType === "Function" ||
+ introType === "eventHandler" ||
+ introType === "setTimeout" ||
+ introType === "setInterval");
+}
+
+exports.isEvalSource = isEvalSource;
+
+function getSourceURL(source, window) {
+ if (isEvalSource(source)) {
+ // Eval sources have no urls, but they might have a `displayURL`
+ // created with the sourceURL pragma. If the introduction script
+ // is a non-eval script, generate an full absolute URL relative to it.
+
+ if (source.displayURL && source.introductionScript &&
+ !isEvalSource(source.introductionScript.source)) {
+
+ if (source.introductionScript.source.url === "debugger eval code") {
+ if (window) {
+ // If this is a named eval script created from the console, make it
+ // relative to the current page. window is only available
+ // when we care about this.
+ return joinURI(window.location.href, source.displayURL);
+ }
+ }
+ else {
+ return joinURI(source.introductionScript.source.url, source.displayURL);
+ }
+ }
+
+ return source.displayURL;
+ }
+ else if (source.url === "debugger eval code") {
+ // Treat code evaluated by the console as unnamed eval scripts
+ return null;
+ }
+ return source.url;
+}
+
+exports.getSourceURL = getSourceURL;
+
+/**
+ * Resolve a URI back to physical file.
+ *
+ * Of course, this works only for URIs pointing to local resources.
+ *
+ * @param aURI
+ * URI to resolve
+ * @return
+ * resolved nsIURI
+ */
+function resolveURIToLocalPath(aURI) {
+ let resolved;
+ switch (aURI.scheme) {
+ case "jar":
+ case "file":
+ return aURI;
+
+ case "chrome":
+ resolved = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIChromeRegistry).convertChromeURL(aURI);
+ return resolveURIToLocalPath(resolved);
+
+ case "resource":
+ resolved = Cc["@mozilla.org/network/protocol;1?name=resource"].
+ getService(Ci.nsIResProtocolHandler).resolveURI(aURI);
+ aURI = Services.io.newURI(resolved, null, null);
+ return resolveURIToLocalPath(aURI);
+
+ default:
+ return null;
+ }
+}
+
+/**
+ * A SourceActor provides information about the source of a script. There
+ * are two kinds of source actors: ones that represent real source objects,
+ * and ones that represent non-existant "original" sources when the real
+ * sources are sourcemapped. When a source is sourcemapped, actors are
+ * created for both the "generated" and "original" sources, and the client will
+ * only see the original sources. We separate these because there isn't
+ * a 1:1 mapping of generated to original sources; one generated source
+ * may represent N original sources, so we need to create N + 1 separate
+ * actors.
+ *
+ * There are 4 different scenarios for sources that you should
+ * understand:
+ *
+ * - A single non-sourcemapped source that is not inlined in HTML
+ * (separate JS file, eval'ed code, etc)
+ * - A single sourcemapped source which creates N original sources
+ * - An HTML page with multiple inline scripts, which are distinct
+ * sources, but should be represented as a single source
+ * - A pretty-printed source (which may or may not be an original
+ * sourcemapped source), which generates a sourcemap for itself
+ *
+ * The complexity of `SourceActor` and `ThreadSources` are to handle
+ * all of thise cases and hopefully internalize the complexities.
+ *
+ * @param Debugger.Source source
+ * The source object we are representing.
+ * @param ThreadActor thread
+ * The current thread actor.
+ * @param String originalUrl
+ * Optional. For sourcemapped urls, the original url this is representing.
+ * @param Debugger.Source generatedSource
+ * Optional, passed in when aSourceMap is also passed in. The generated
+ * source object that introduced this source.
+ * @param Boolean isInlineSource
+ * Optional. True if this is an inline source from a HTML or XUL page.
+ * @param String contentType
+ * Optional. The content type of this source, if immediately available.
+ */
+let SourceActor = ActorClassWithSpec(sourceSpec, {
+ typeName: "source",
+
+ initialize: function ({ source, thread, originalUrl, generatedSource,
+ isInlineSource, contentType }) {
+ this._threadActor = thread;
+ this._originalUrl = originalUrl;
+ this._source = source;
+ this._generatedSource = generatedSource;
+ this._contentType = contentType;
+ this._isInlineSource = isInlineSource;
+
+ this.onSource = this.onSource.bind(this);
+ this._invertSourceMap = this._invertSourceMap.bind(this);
+ this._encodeAndSetSourceMapURL = this._encodeAndSetSourceMapURL.bind(this);
+ this._getSourceText = this._getSourceText.bind(this);
+
+ this._mapSourceToAddon();
+
+ if (this.threadActor.sources.isPrettyPrinted(this.url)) {
+ this._init = this.prettyPrint(
+ this.threadActor.sources.prettyPrintIndent(this.url)
+ ).then(null, error => {
+ DevToolsUtils.reportException("SourceActor", error);
+ });
+ } else {
+ this._init = null;
+ }
+ },
+
+ get isSourceMapped() {
+ return !!(!this.isInlineSource && (
+ this._originalURL || this._generatedSource ||
+ this.threadActor.sources.isPrettyPrinted(this.url)
+ ));
+ },
+
+ get isInlineSource() {
+ return this._isInlineSource;
+ },
+
+ get threadActor() { return this._threadActor; },
+ get sources() { return this._threadActor.sources; },
+ get dbg() { return this.threadActor.dbg; },
+ get source() { return this._source; },
+ get generatedSource() { return this._generatedSource; },
+ get breakpointActorMap() { return this.threadActor.breakpointActorMap; },
+ get url() {
+ if (this.source) {
+ return getSourceURL(this.source, this.threadActor._parent.window);
+ }
+ return this._originalUrl;
+ },
+ get addonID() { return this._addonID; },
+ get addonPath() { return this._addonPath; },
+
+ get prettyPrintWorker() {
+ return this.threadActor.prettyPrintWorker;
+ },
+
+ form: function () {
+ let source = this.source || this.generatedSource;
+ // This might not have a source or a generatedSource because we
+ // treat HTML pages with inline scripts as a special SourceActor
+ // that doesn't have either
+ let introductionUrl = null;
+ if (source && source.introductionScript) {
+ introductionUrl = source.introductionScript.source.url;
+ }
+
+ return {
+ actor: this.actorID,
+ generatedUrl: this.generatedSource ? this.generatedSource.url : null,
+ url: this.url ? this.url.split(" -> ").pop() : null,
+ addonID: this._addonID,
+ addonPath: this._addonPath,
+ isBlackBoxed: this.threadActor.sources.isBlackBoxed(this.url),
+ isPrettyPrinted: this.threadActor.sources.isPrettyPrinted(this.url),
+ isSourceMapped: this.isSourceMapped,
+ sourceMapURL: source ? source.sourceMapURL : null,
+ introductionUrl: introductionUrl ? introductionUrl.split(" -> ").pop() : null,
+ introductionType: source ? source.introductionType : null
+ };
+ },
+
+ disconnect: function () {
+ if (this.registeredPool && this.registeredPool.sourceActors) {
+ delete this.registeredPool.sourceActors[this.actorID];
+ }
+ },
+
+ _mapSourceToAddon: function () {
+ try {
+ var nsuri = Services.io.newURI(this.url.split(" -> ").pop(), null, null);
+ }
+ catch (e) {
+ // We can't do anything with an invalid URI
+ return;
+ }
+
+ let localURI = resolveURIToLocalPath(nsuri);
+ if (!localURI) {
+ return;
+ }
+
+ let id = mapURIToAddonID(localURI);
+ if (!id) {
+ return;
+ }
+ this._addonID = id;
+
+ if (localURI instanceof Ci.nsIJARURI) {
+ // The path in the add-on is easy for jar: uris
+ this._addonPath = localURI.JAREntry;
+ }
+ else if (localURI instanceof Ci.nsIFileURL) {
+ // For file: uris walk up to find the last directory that is part of the
+ // add-on
+ let target = localURI.file;
+ let path = target.leafName;
+
+ // We can assume that the directory containing the source file is part
+ // of the add-on
+ let root = target.parent;
+ let file = root.parent;
+ while (file && mapURIToAddonID(Services.io.newFileURI(file))) {
+ path = root.leafName + "/" + path;
+ root = file;
+ file = file.parent;
+ }
+
+ if (!file) {
+ const error = new Error("Could not find the root of the add-on for " + this.url);
+ DevToolsUtils.reportException("SourceActor.prototype._mapSourceToAddon", error);
+ return;
+ }
+
+ this._addonPath = path;
+ }
+ },
+
+ _reportLoadSourceError: function (error, map = null) {
+ try {
+ DevToolsUtils.reportException("SourceActor", error);
+
+ JSON.stringify(this.form(), null, 4).split(/\n/g)
+ .forEach(line => console.error("\t", line));
+
+ if (!map) {
+ return;
+ }
+
+ console.error("\t", "source map's sourceRoot =", map.sourceRoot);
+
+ console.error("\t", "source map's sources =");
+ map.sources.forEach(s => {
+ let hasSourceContent = map.sourceContentFor(s, true);
+ console.error("\t\t", s, "\t",
+ hasSourceContent ? "has source content" : "no source content");
+ });
+
+ console.error("\t", "source map's sourcesContent =");
+ map.sourcesContent.forEach(c => {
+ if (c.length > 80) {
+ c = c.slice(0, 77) + "...";
+ }
+ c = c.replace(/\n/g, "\\n");
+ console.error("\t\t", c);
+ });
+ } catch (e) { }
+ },
+
+ _getSourceText: function () {
+ let toResolvedContent = t => ({
+ content: t,
+ contentType: this._contentType
+ });
+
+ let genSource = this.generatedSource || this.source;
+ return this.threadActor.sources.fetchSourceMap(genSource).then(map => {
+ if (map) {
+ try {
+ let sourceContent = map.sourceContentFor(this.url);
+ if (sourceContent) {
+ return toResolvedContent(sourceContent);
+ }
+ } catch (error) {
+ this._reportLoadSourceError(error, map);
+ throw error;
+ }
+ }
+
+ // Use `source.text` if it exists, is not the "no source" string, and
+ // the content type of the source is JavaScript or it is synthesized
+ // wasm. It will be "no source" if the Debugger API wasn't able to load
+ // the source because sources were discarded
+ // (javascript.options.discardSystemSource == true). Re-fetch non-JS
+ // sources to get the contentType from the headers.
+ if (this.source &&
+ this.source.text !== "[no source]" &&
+ this._contentType &&
+ (this._contentType.indexOf("javascript") !== -1 ||
+ this._contentType === "text/wasm")) {
+ return toResolvedContent(this.source.text);
+ }
+ else {
+ // Only load the HTML page source from cache (which exists when
+ // there are inline sources). Otherwise, we can't trust the
+ // cache because we are most likely here because we are
+ // fetching the original text for sourcemapped code, and the
+ // page hasn't requested it before (if it has, it was a
+ // previous debugging session).
+ let loadFromCache = this.isInlineSource;
+
+ // Fetch the sources with the same principal as the original document
+ let win = this.threadActor._parent.window;
+ let principal, cacheKey;
+ // On xpcshell, we don't have a window but a Sandbox
+ if (!isWorker && win instanceof Ci.nsIDOMWindow) {
+ let webNav = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation);
+ let channel = webNav.currentDocumentChannel;
+ principal = channel.loadInfo.loadingPrincipal;
+
+ // Retrieve the cacheKey in order to load POST requests from cache
+ // Note that chrome:// URLs don't support this interface.
+ if (loadFromCache &&
+ webNav.currentDocumentChannel instanceof Ci.nsICacheInfoChannel) {
+ cacheKey = webNav.currentDocumentChannel.cacheKey;
+ assert(
+ cacheKey,
+ "Could not fetch the cacheKey from the related document."
+ );
+ }
+ }
+
+ let sourceFetched = fetch(this.url, {
+ principal,
+ cacheKey,
+ loadFromCache
+ });
+
+ // Record the contentType we just learned during fetching
+ return sourceFetched
+ .then(result => {
+ this._contentType = result.contentType;
+ return result;
+ }, error => {
+ this._reportLoadSourceError(error, map);
+ throw error;
+ });
+ }
+ });
+ },
+
+ /**
+ * Get all executable lines from the current source
+ * @return Array - Executable lines of the current script
+ **/
+ getExecutableLines: function () {
+ function sortLines(lines) {
+ // Converting the Set into an array
+ lines = [...lines];
+ lines.sort((a, b) => {
+ return a - b;
+ });
+ return lines;
+ }
+
+ if (this.generatedSource) {
+ return this.threadActor.sources.getSourceMap(this.generatedSource).then(sm => {
+ let lines = new Set();
+
+ // Position of executable lines in the generated source
+ let offsets = this.getExecutableOffsets(this.generatedSource, false);
+ for (let offset of offsets) {
+ let {line, source: sourceUrl} = sm.originalPositionFor({
+ line: offset.lineNumber,
+ column: offset.columnNumber
+ });
+
+ if (sourceUrl === this.url) {
+ lines.add(line);
+ }
+ }
+
+ return sortLines(lines);
+ });
+ }
+
+ let lines = this.getExecutableOffsets(this.source, true);
+ return sortLines(lines);
+ },
+
+ /**
+ * Extract all executable offsets from the given script
+ * @param String url - extract offsets of the script with this url
+ * @param Boolean onlyLine - will return only the line number
+ * @return Set - Executable offsets/lines of the script
+ **/
+ getExecutableOffsets: function (source, onlyLine) {
+ let offsets = new Set();
+ for (let s of this.dbg.findScripts({ source })) {
+ for (let offset of s.getAllColumnOffsets()) {
+ offsets.add(onlyLine ? offset.lineNumber : offset);
+ }
+ }
+
+ return offsets;
+ },
+
+ /**
+ * Handler for the "source" packet.
+ */
+ onSource: function () {
+ return resolve(this._init)
+ .then(this._getSourceText)
+ .then(({ content, contentType }) => {
+ return {
+ source: createValueGrip(content, this.threadActor.threadLifetimePool,
+ this.threadActor.objectGrip),
+ contentType: contentType
+ };
+ })
+ .then(null, aError => {
+ reportError(aError, "Got an exception during SA_onSource: ");
+ throw new Error("Could not load the source for " + this.url + ".\n" +
+ DevToolsUtils.safeErrorString(aError));
+ });
+ },
+
+ /**
+ * Handler for the "prettyPrint" packet.
+ */
+ prettyPrint: function (indent) {
+ this.threadActor.sources.prettyPrint(this.url, indent);
+ return this._getSourceText()
+ .then(this._sendToPrettyPrintWorker(indent))
+ .then(this._invertSourceMap)
+ .then(this._encodeAndSetSourceMapURL)
+ .then(() => {
+ // We need to reset `_init` now because we have already done the work of
+ // pretty printing, and don't want onSource to wait forever for
+ // initialization to complete.
+ this._init = null;
+ })
+ .then(this.onSource)
+ .then(null, error => {
+ this.disablePrettyPrint();
+ throw new Error(DevToolsUtils.safeErrorString(error));
+ });
+ },
+
+ /**
+ * Return a function that sends a request to the pretty print worker, waits on
+ * the worker's response, and then returns the pretty printed code.
+ *
+ * @param Number aIndent
+ * The number of spaces to indent by the code by, when we send the
+ * request to the pretty print worker.
+ * @returns Function
+ * Returns a function which takes an AST, and returns a promise that
+ * is resolved with `{ code, mappings }` where `code` is the pretty
+ * printed code, and `mappings` is an array of source mappings.
+ */
+ _sendToPrettyPrintWorker: function (aIndent) {
+ return ({ content }) => {
+ return this.prettyPrintWorker.performTask("pretty-print", {
+ url: this.url,
+ indent: aIndent,
+ source: content
+ });
+ };
+ },
+
+ /**
+ * Invert a source map. So if a source map maps from a to b, return a new
+ * source map from b to a. We need to do this because the source map we get
+ * from _generatePrettyCodeAndMap goes the opposite way we want it to for
+ * debugging.
+ *
+ * Note that the source map is modified in place.
+ */
+ _invertSourceMap: function ({ code, mappings }) {
+ const generator = new SourceMapGenerator({ file: this.url });
+ return DevToolsUtils.yieldingEach(mappings._array, m => {
+ let mapping = {
+ generated: {
+ line: m.originalLine,
+ column: m.originalColumn
+ }
+ };
+ if (m.source) {
+ mapping.source = m.source;
+ mapping.original = {
+ line: m.generatedLine,
+ column: m.generatedColumn
+ };
+ mapping.name = m.name;
+ }
+ generator.addMapping(mapping);
+ }).then(() => {
+ generator.setSourceContent(this.url, code);
+ let consumer = SourceMapConsumer.fromSourceMap(generator);
+
+ return {
+ code: code,
+ map: consumer
+ };
+ });
+ },
+
+ /**
+ * Save the source map back to our thread's ThreadSources object so that
+ * stepping, breakpoints, debugger statements, etc can use it. If we are
+ * pretty printing a source mapped source, we need to compose the existing
+ * source map with our new one.
+ */
+ _encodeAndSetSourceMapURL: function ({ map: sm }) {
+ let source = this.generatedSource || this.source;
+ let sources = this.threadActor.sources;
+
+ return sources.getSourceMap(source).then(prevMap => {
+ if (prevMap) {
+ // Compose the source maps
+ this._oldSourceMapping = {
+ url: source.sourceMapURL,
+ map: prevMap
+ };
+
+ prevMap = SourceMapGenerator.fromSourceMap(prevMap);
+ prevMap.applySourceMap(sm, this.url);
+ sm = SourceMapConsumer.fromSourceMap(prevMap);
+ }
+
+ let sources = this.threadActor.sources;
+ sources.clearSourceMapCache(source.sourceMapURL);
+ sources.setSourceMapHard(source, null, sm);
+ });
+ },
+
+ /**
+ * Handler for the "disablePrettyPrint" packet.
+ */
+ disablePrettyPrint: function () {
+ let source = this.generatedSource || this.source;
+ let sources = this.threadActor.sources;
+ let sm = sources.getSourceMap(source);
+
+ sources.clearSourceMapCache(source.sourceMapURL, { hard: true });
+
+ if (this._oldSourceMapping) {
+ sources.setSourceMapHard(source,
+ this._oldSourceMapping.url,
+ this._oldSourceMapping.map);
+ this._oldSourceMapping = null;
+ }
+
+ this.threadActor.sources.disablePrettyPrint(this.url);
+ return this.onSource();
+ },
+
+ /**
+ * Handler for the "blackbox" packet.
+ */
+ blackbox: function () {
+ this.threadActor.sources.blackBox(this.url);
+ if (this.threadActor.state == "paused"
+ && this.threadActor.youngestFrame
+ && this.threadActor.youngestFrame.script.url == this.url) {
+ return true;
+ }
+ return false;
+ },
+
+ /**
+ * Handler for the "unblackbox" packet.
+ */
+ unblackbox: function () {
+ this.threadActor.sources.unblackBox(this.url);
+ },
+
+ /**
+ * Handle a request to set a breakpoint.
+ *
+ * @param Number line
+ * Line to break on.
+ * @param Number column
+ * Column to break on.
+ * @param String condition
+ * A condition which must be true for breakpoint to be hit.
+ * @param Boolean noSliding
+ * If true, disables breakpoint sliding.
+ *
+ * @returns Promise
+ * A promise that resolves to a JSON object representing the
+ * response.
+ */
+ setBreakpoint: function (line, column, condition, noSliding) {
+ if (this.threadActor.state !== "paused") {
+ throw {
+ error: "wrongState",
+ message: "Cannot set breakpoint while debuggee is running."
+ };
+ }
+
+ let location = new OriginalLocation(this, line, column);
+ return this._getOrCreateBreakpointActor(
+ location,
+ condition,
+ noSliding
+ ).then((actor) => {
+ let response = {
+ actor: actor.actorID,
+ isPending: actor.isPending
+ };
+
+ let actualLocation = actor.originalLocation;
+ if (!actualLocation.equals(location)) {
+ response.actualLocation = actualLocation.toJSON();
+ }
+
+ return response;
+ });
+ },
+
+ /**
+ * Get or create a BreakpointActor for the given location in the original
+ * source, and ensure it is set as a breakpoint handler on all scripts that
+ * match the given location.
+ *
+ * @param OriginalLocation originalLocation
+ * An OriginalLocation representing the location of the breakpoint in
+ * the original source.
+ * @param String condition
+ * A string that is evaluated whenever the breakpoint is hit. If the
+ * string evaluates to false, the breakpoint is ignored.
+ * @param Boolean noSliding
+ * If true, disables breakpoint sliding.
+ *
+ * @returns BreakpointActor
+ * A BreakpointActor representing the breakpoint.
+ */
+ _getOrCreateBreakpointActor: function (originalLocation, condition, noSliding) {
+ let actor = this.breakpointActorMap.getActor(originalLocation);
+ if (!actor) {
+ actor = new BreakpointActor(this.threadActor, originalLocation);
+ this.threadActor.threadLifetimePool.addActor(actor);
+ this.breakpointActorMap.setActor(originalLocation, actor);
+ }
+
+ actor.condition = condition;
+
+ return this._setBreakpoint(actor, noSliding);
+ },
+
+ /*
+ * Ensure the given BreakpointActor is set as a breakpoint handler on all
+ * scripts that match its location in the original source.
+ *
+ * If there are no scripts that match the location of the BreakpointActor,
+ * we slide its location to the next closest line (for line breakpoints) or
+ * column (for column breakpoint) that does.
+ *
+ * If breakpoint sliding fails, then either there are no scripts that contain
+ * any code for the given location, or they were all garbage collected before
+ * the debugger started running. We cannot distinguish between these two
+ * cases, so we insert the BreakpointActor in the BreakpointActorMap as
+ * a pending breakpoint. Whenever a new script is introduced, this method is
+ * called again for each pending breakpoint.
+ *
+ * @param BreakpointActor actor
+ * The BreakpointActor to be set as a breakpoint handler.
+ * @param Boolean noSliding
+ * If true, disables breakpoint sliding.
+ *
+ * @returns A Promise that resolves to the given BreakpointActor.
+ */
+ _setBreakpoint: function (actor, noSliding) {
+ const { originalLocation } = actor;
+ const { originalLine, originalSourceActor } = originalLocation;
+
+ if (!this.isSourceMapped) {
+ const generatedLocation = GeneratedLocation.fromOriginalLocation(originalLocation);
+ if (!this._setBreakpointAtGeneratedLocation(actor, generatedLocation) &&
+ !noSliding) {
+ const query = { line: originalLine };
+ // For most cases, we have a real source to query for. The
+ // only time we don't is for HTML pages. In that case we want
+ // to query for scripts in an HTML page based on its URL, as
+ // there could be several sources within an HTML page.
+ if (this.source) {
+ query.source = this.source;
+ } else {
+ query.url = this.url;
+ }
+ const scripts = this.dbg.findScripts(query);
+
+ // Never do breakpoint sliding for column breakpoints.
+ // Additionally, never do breakpoint sliding if no scripts
+ // exist on this line.
+ //
+ // Sliding can go horribly wrong if we always try to find the
+ // next line with valid entry points in the entire file.
+ // Scripts may be completely GCed and we never knew they
+ // existed, so we end up sliding through whole functions to
+ // the user's bewilderment.
+ //
+ // We can slide reliably if any scripts exist, however, due
+ // to how scripts are kept alive. A parent Debugger.Script
+ // keeps all of its children alive, so as long as we have a
+ // valid script, we can slide through it and know we won't
+ // slide through any of its child scripts. Additionally, if a
+ // script gets GCed, that means that all parents scripts are
+ // GCed as well, and no scripts will exist on those lines
+ // anymore. We will never slide through a GCed script.
+ if (originalLocation.originalColumn || scripts.length === 0) {
+ return promise.resolve(actor);
+ }
+
+ // Find the script that spans the largest amount of code to
+ // determine the bounds for sliding.
+ const largestScript = scripts.reduce((largestScript, script) => {
+ if (script.lineCount > largestScript.lineCount) {
+ return script;
+ }
+ return largestScript;
+ });
+ const maxLine = largestScript.startLine + largestScript.lineCount - 1;
+
+ let actualLine = originalLine;
+ for (; actualLine <= maxLine; actualLine++) {
+ const loc = new GeneratedLocation(this, actualLine);
+ if (this._setBreakpointAtGeneratedLocation(actor, loc)) {
+ break;
+ }
+ }
+
+ // The above loop should never complete. We only did breakpoint sliding
+ // because we found scripts on the line we started from,
+ // which means there must be valid entry points somewhere
+ // within those scripts.
+ assert(
+ actualLine <= maxLine,
+ "Could not find any entry points to set a breakpoint on, " +
+ "even though I was told a script existed on the line I started " +
+ "the search with."
+ );
+
+ // Update the actor to use the new location (reusing a
+ // previous breakpoint if it already exists on that line).
+ const actualLocation = new OriginalLocation(originalSourceActor, actualLine);
+ const existingActor = this.breakpointActorMap.getActor(actualLocation);
+ this.breakpointActorMap.deleteActor(originalLocation);
+ if (existingActor) {
+ actor.delete();
+ actor = existingActor;
+ } else {
+ actor.originalLocation = actualLocation;
+ this.breakpointActorMap.setActor(actualLocation, actor);
+ }
+ }
+
+ return promise.resolve(actor);
+ } else {
+ return this.sources.getAllGeneratedLocations(originalLocation).then((generatedLocations) => {
+ this._setBreakpointAtAllGeneratedLocations(
+ actor,
+ generatedLocations
+ );
+
+ return actor;
+ });
+ }
+ },
+
+ _setBreakpointAtAllGeneratedLocations: function (actor, generatedLocations) {
+ let success = false;
+ for (let generatedLocation of generatedLocations) {
+ if (this._setBreakpointAtGeneratedLocation(
+ actor,
+ generatedLocation
+ )) {
+ success = true;
+ }
+ }
+ return success;
+ },
+
+ /*
+ * Ensure the given BreakpointActor is set as breakpoint handler on all
+ * scripts that match the given location in the generated source.
+ *
+ * @param BreakpointActor actor
+ * The BreakpointActor to be set as a breakpoint handler.
+ * @param GeneratedLocation generatedLocation
+ * A GeneratedLocation representing the location in the generated
+ * source for which the given BreakpointActor is to be set as a
+ * breakpoint handler.
+ *
+ * @returns A Boolean that is true if the BreakpointActor was set as a
+ * breakpoint handler on at least one script, and false otherwise.
+ */
+ _setBreakpointAtGeneratedLocation: function (actor, generatedLocation) {
+ let {
+ generatedSourceActor,
+ generatedLine,
+ generatedColumn,
+ generatedLastColumn
+ } = generatedLocation;
+
+ // Find all scripts that match the given source actor and line
+ // number.
+ const query = { line: generatedLine };
+ if (generatedSourceActor.source) {
+ query.source = generatedSourceActor.source;
+ } else {
+ query.url = generatedSourceActor.url;
+ }
+ let scripts = this.dbg.findScripts(query);
+
+ scripts = scripts.filter((script) => !actor.hasScript(script));
+
+ // Find all entry points that correspond to the given location.
+ let entryPoints = [];
+ if (generatedColumn === undefined) {
+ // This is a line breakpoint, so we are interested in all offsets
+ // that correspond to the given line number.
+ for (let script of scripts) {
+ let offsets = script.getLineOffsets(generatedLine);
+ if (offsets.length > 0) {
+ entryPoints.push({ script, offsets });
+ }
+ }
+ } else {
+ // This is a column breakpoint, so we are interested in all column
+ // offsets that correspond to the given line *and* column number.
+ for (let script of scripts) {
+ let columnToOffsetMap = script.getAllColumnOffsets()
+ .filter(({ lineNumber }) => {
+ return lineNumber === generatedLine;
+ });
+ for (let { columnNumber: column, offset } of columnToOffsetMap) {
+ if (column >= generatedColumn && column <= generatedLastColumn) {
+ entryPoints.push({ script, offsets: [offset] });
+ }
+ }
+ }
+ }
+
+ if (entryPoints.length === 0) {
+ return false;
+ }
+ setBreakpointAtEntryPoints(actor, entryPoints);
+ return true;
+ }
+});
+
+exports.SourceActor = SourceActor;