import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

View file

@ -0,0 +1,833 @@
/* 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 { Ci, Cu } = require("chrome");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { assert, fetch } = DevToolsUtils;
const EventEmitter = require("devtools/shared/event-emitter");
const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
const { resolve } = require("promise");
const { joinURI } = require("devtools/shared/path");
loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/source", true);
loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/source", true);
loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);
/**
* Manages the sources for a thread. Handles source maps, locations in the
* sources, etc for ThreadActors.
*/
function TabSources(threadActor, allowSourceFn = () => true) {
EventEmitter.decorate(this);
this._thread = threadActor;
this._useSourceMaps = true;
this._autoBlackBox = true;
this._anonSourceMapId = 1;
this.allowSource = source => {
return !isHiddenSource(source) && allowSourceFn(source);
};
this.blackBoxedSources = new Set();
this.prettyPrintedSources = new Map();
this.neverAutoBlackBoxSources = new Set();
// generated Debugger.Source -> promise of SourceMapConsumer
this._sourceMaps = new Map();
// sourceMapURL -> promise of SourceMapConsumer
this._sourceMapCache = Object.create(null);
// Debugger.Source -> SourceActor
this._sourceActors = new Map();
// url -> SourceActor
this._sourceMappedSourceActors = Object.create(null);
}
/**
* Matches strings of the form "foo.min.js" or "foo-min.js", etc. If the regular
* expression matches, we can be fairly sure that the source is minified, and
* treat it as such.
*/
const MINIFIED_SOURCE_REGEXP = /\bmin\.js$/;
TabSources.prototype = {
/**
* Update preferences and clear out existing sources
*/
setOptions: function (options) {
let shouldReset = false;
if ("useSourceMaps" in options) {
shouldReset = true;
this._useSourceMaps = options.useSourceMaps;
}
if ("autoBlackBox" in options) {
shouldReset = true;
this._autoBlackBox = options.autoBlackBox;
}
if (shouldReset) {
this.reset();
}
},
/**
* Clear existing sources so they are recreated on the next access.
*
* @param Object opts
* Specify { sourceMaps: true } if you also want to clear
* the source map cache (usually done on reload).
*/
reset: function (opts = {}) {
this._sourceActors = new Map();
this._sourceMaps = new Map();
this._sourceMappedSourceActors = Object.create(null);
if (opts.sourceMaps) {
this._sourceMapCache = Object.create(null);
}
},
/**
* Return the source actor representing the `source` (or
* `originalUrl`), creating one if none exists already. May return
* null if the source is disallowed.
*
* @param Debugger.Source source
* The source to make an actor for
* @param String originalUrl
* The original source URL of a sourcemapped source
* @param optional Debguger.Source generatedSource
* The generated source that introduced this source via source map,
* if any.
* @param optional String contentType
* The content type of the source, if immediately available.
* @returns a SourceActor representing the source or null.
*/
source: function ({ source, originalUrl, generatedSource,
isInlineSource, contentType }) {
assert(source || (originalUrl && generatedSource),
"TabSources.prototype.source needs an originalUrl or a source");
if (source) {
// If a source is passed, we are creating an actor for a real
// source, which may or may not be sourcemapped.
if (!this.allowSource(source)) {
return null;
}
// It's a hack, but inline HTML scripts each have real sources,
// but we want to represent all of them as one source as the
// HTML page. The actor representing this fake HTML source is
// stored in this array, which always has a URL, so check it
// first.
if (source.url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[source.url];
}
if (isInlineSource) {
// If it's an inline source, the fake HTML source hasn't been
// created yet (would have returned above), so flip this source
// into a sourcemapped state by giving it an `originalUrl` which
// is the HTML url.
originalUrl = source.url;
source = null;
}
else if (this._sourceActors.has(source)) {
return this._sourceActors.get(source);
}
}
else if (originalUrl) {
// Not all "original" scripts are distinctly separate from the
// generated script. Pretty-printed sources have a sourcemap for
// themselves, so we need to make sure there a real source
// doesn't already exist with this URL.
for (let [source, actor] of this._sourceActors) {
if (source.url === originalUrl) {
return actor;
}
}
if (originalUrl in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[originalUrl];
}
}
let actor = new SourceActor({
thread: this._thread,
source: source,
originalUrl: originalUrl,
generatedSource: generatedSource,
isInlineSource: isInlineSource,
contentType: contentType
});
let sourceActorStore = this._thread.sourceActorStore;
var id = sourceActorStore.getReusableActorId(source, originalUrl);
if (id) {
actor.actorID = id;
}
this._thread.threadLifetimePool.addActor(actor);
sourceActorStore.setReusableActorId(source, originalUrl, actor.actorID);
if (this._autoBlackBox &&
!this.neverAutoBlackBoxSources.has(actor.url) &&
this._isMinifiedURL(actor.url)) {
this.blackBox(actor.url);
this.neverAutoBlackBoxSources.add(actor.url);
}
if (source) {
this._sourceActors.set(source, actor);
}
else {
this._sourceMappedSourceActors[originalUrl] = actor;
}
this._emitNewSource(actor);
return actor;
},
_emitNewSource: function (actor) {
if (!actor.source) {
// Always notify if we don't have a source because that means
// it's something that has been sourcemapped, or it represents
// the HTML file that contains inline sources.
this.emit("newSource", actor);
}
else {
// If sourcemapping is enabled and a source has sourcemaps, we
// create `SourceActor` instances for both the original and
// generated sources. The source actors for the generated
// sources are only for internal use, however; breakpoints are
// managed by these internal actors. We only want to notify the
// user of the original sources though, so if the actor has a
// `Debugger.Source` instance and a valid source map (meaning
// it's a generated source), don't send the notification.
this.fetchSourceMap(actor.source).then(map => {
if (!map) {
this.emit("newSource", actor);
}
});
}
},
getSourceActor: function (source) {
if (source.url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[source.url];
}
if (this._sourceActors.has(source)) {
return this._sourceActors.get(source);
}
throw new Error("getSource: could not find source actor for " +
(source.url || "source"));
},
getSourceActorByURL: function (url) {
if (url) {
for (let [source, actor] of this._sourceActors) {
if (source.url === url) {
return actor;
}
}
if (url in this._sourceMappedSourceActors) {
return this._sourceMappedSourceActors[url];
}
}
throw new Error("getSourceActorByURL: could not find source for " + url);
return null;
},
/**
* Returns true if the URL likely points to a minified resource, false
* otherwise.
*
* @param String aURL
* The URL to test.
* @returns Boolean
*/
_isMinifiedURL: function (aURL) {
if (!aURL) {
return false;
}
try {
let url = new URL(aURL);
let pathname = url.pathname;
return MINIFIED_SOURCE_REGEXP.test(pathname.slice(pathname.lastIndexOf("/") + 1));
} catch (e) {
// Not a valid URL so don't try to parse out the filename, just test the
// whole thing with the minified source regexp.
return MINIFIED_SOURCE_REGEXP.test(aURL);
}
},
/**
* Create a source actor representing this source. This ignores
* source mapping and always returns an actor representing this real
* source. Use `createSourceActors` if you want to respect source maps.
*
* @param Debugger.Source aSource
* The source instance to create an actor for.
* @returns SourceActor
*/
createNonSourceMappedActor: function (aSource) {
// Don't use getSourceURL because we don't want to consider the
// displayURL property if it's an eval source. We only want to
// consider real URLs, otherwise if there is a URL but it's
// invalid the code below will not set the content type, and we
// will later try to fetch the contents of the URL to figure out
// the content type, but it's a made up URL for eval sources.
let url = isEvalSource(aSource) ? null : aSource.url;
let spec = { source: aSource };
// XXX bug 915433: We can't rely on Debugger.Source.prototype.text
// if the source is an HTML-embedded <script> tag. Since we don't
// have an API implemented to detect whether this is the case, we
// need to be conservative and only treat valid js files as real
// sources. Otherwise, use the `originalUrl` property to treat it
// as an HTML source that manages multiple inline sources.
// Assume the source is inline if the element that introduced it is not a
// script element, or does not have a src attribute.
let element = aSource.element ? aSource.element.unsafeDereference() : null;
if (element && (element.tagName !== "SCRIPT" || !element.hasAttribute("src"))) {
spec.isInlineSource = true;
} else if (aSource.introductionType === "wasm") {
// Wasm sources are not JavaScript. Give them their own content-type.
spec.contentType = "text/wasm";
} else {
if (url) {
// There are a few special URLs that we know are JavaScript:
// inline `javascript:` and code coming from the console
if (url.indexOf("Scratchpad/") === 0 ||
url.indexOf("javascript:") === 0 ||
url === "debugger eval code") {
spec.contentType = "text/javascript";
} else {
try {
let pathname = new URL(url).pathname;
let filename = pathname.slice(pathname.lastIndexOf("/") + 1);
let index = filename.lastIndexOf(".");
let extension = index >= 0 ? filename.slice(index + 1) : "";
if (extension === "xml") {
// XUL inline scripts may not correctly have the
// `source.element` property, so do a blunt check here if
// it's an xml page.
spec.isInlineSource = true;
}
else if (extension === "js") {
spec.contentType = "text/javascript";
}
} catch (e) {
// This only needs to be here because URL is not yet exposed to
// workers. (BUG 1258892)
const filename = url;
const index = filename.lastIndexOf(".");
const extension = index >= 0 ? filename.slice(index + 1) : "";
if (extension === "js") {
spec.contentType = "text/javascript";
}
}
}
}
else {
// Assume the content is javascript if there's no URL
spec.contentType = "text/javascript";
}
}
return this.source(spec);
},
/**
* This is an internal function that returns a promise of an array
* of source actors representing all the source mapped sources of
* `aSource`, or `null` if the source is not sourcemapped or
* sourcemapping is disabled. Users should call `createSourceActors`
* instead of this.
*
* @param Debugger.Source aSource
* The source instance to create actors for.
* @return Promise of an array of source actors
*/
_createSourceMappedActors: function (aSource) {
if (!this._useSourceMaps || !aSource.sourceMapURL) {
return resolve(null);
}
return this.fetchSourceMap(aSource)
.then(map => {
if (map) {
return map.sources.map(s => {
return this.source({ originalUrl: s, generatedSource: aSource });
}).filter(isNotNull);
}
return null;
});
},
/**
* Creates the source actors representing the appropriate sources
* of `aSource`. If sourcemapped, returns actors for all of the original
* sources, otherwise returns a 1-element array with the actor for
* `aSource`.
*
* @param Debugger.Source aSource
* The source instance to create actors for.
* @param Promise of an array of source actors
*/
createSourceActors: function (aSource) {
return this._createSourceMappedActors(aSource).then(actors => {
let actor = this.createNonSourceMappedActor(aSource);
return (actors || [actor]).filter(isNotNull);
});
},
/**
* Return a promise of a SourceMapConsumer for the source map for
* `aSource`; if we already have such a promise extant, return that.
* This will fetch the source map if we don't have a cached object
* and source maps are enabled (see `_fetchSourceMap`).
*
* @param Debugger.Source aSource
* The source instance to get sourcemaps for.
* @return Promise of a SourceMapConsumer
*/
fetchSourceMap: function (aSource) {
if (!this._useSourceMaps) {
return resolve(null);
}
else if (this._sourceMaps.has(aSource)) {
return this._sourceMaps.get(aSource);
}
else if (!aSource || !aSource.sourceMapURL) {
return resolve(null);
}
let sourceMapURL = aSource.sourceMapURL;
if (aSource.url) {
sourceMapURL = joinURI(aSource.url, sourceMapURL);
}
let result = this._fetchSourceMap(sourceMapURL, aSource.url);
// The promises in `_sourceMaps` must be the exact same instances
// as returned by `_fetchSourceMap` for `clearSourceMapCache` to
// work.
this._sourceMaps.set(aSource, result);
return result;
},
/**
* Return a promise of a SourceMapConsumer for the source map for
* `aSource`. The resolved result may be null if the source does not
* have a source map or source maps are disabled.
*/
getSourceMap: function (aSource) {
return resolve(this._sourceMaps.get(aSource));
},
/**
* Set a SourceMapConsumer for the source map for
* |aSource|.
*/
setSourceMap: function (aSource, aMap) {
this._sourceMaps.set(aSource, resolve(aMap));
},
/**
* Return a promise of a SourceMapConsumer for the source map located at
* |aAbsSourceMapURL|, which must be absolute. If there is already such a
* promise extant, return it. This will not fetch if source maps are
* disabled.
*
* @param string aAbsSourceMapURL
* The source map URL, in absolute form, not relative.
* @param string aScriptURL
* When the source map URL is a data URI, there is no sourceRoot on the
* source map, and the source map's sources are relative, we resolve
* them from aScriptURL.
*/
_fetchSourceMap: function (aAbsSourceMapURL, aSourceURL) {
assert(this._useSourceMaps,
"Cannot fetch sourcemaps if they are disabled");
if (this._sourceMapCache[aAbsSourceMapURL]) {
return this._sourceMapCache[aAbsSourceMapURL];
}
let fetching = fetch(aAbsSourceMapURL, { loadFromCache: false })
.then(({ content }) => {
let map = new SourceMapConsumer(content);
this._setSourceMapRoot(map, aAbsSourceMapURL, aSourceURL);
return map;
})
.then(null, error => {
if (!DevToolsUtils.reportingDisabled) {
DevToolsUtils.reportException("TabSources.prototype._fetchSourceMap", error);
}
return null;
});
this._sourceMapCache[aAbsSourceMapURL] = fetching;
return fetching;
},
/**
* Sets the source map's sourceRoot to be relative to the source map url.
*/
_setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) {
// No need to do this fiddling if we won't be fetching any sources over the
// wire.
if (aSourceMap.hasContentsOfAllSources()) {
return;
}
const base = this._dirname(
aAbsSourceMapURL.indexOf("data:") === 0
? aScriptURL
: aAbsSourceMapURL);
aSourceMap.sourceRoot = aSourceMap.sourceRoot
? joinURI(base, aSourceMap.sourceRoot)
: base;
},
_dirname: function (aPath) {
let url = new URL(aPath);
let href = url.href;
return href.slice(0, href.lastIndexOf("/"));
},
/**
* Clears the source map cache. Source maps are cached by URL so
* they can be reused across separate Debugger instances (once in
* this cache, they will never be reparsed again). They are
* also cached by Debugger.Source objects for usefulness. By default
* this just removes the Debugger.Source cache, but you can remove
* the lower-level URL cache with the `hard` option.
*
* @param aSourceMapURL string
* The source map URL to uncache
* @param opts object
* An object with the following properties:
* - hard: Also remove the lower-level URL cache, which will
* make us completely forget about the source map.
*/
clearSourceMapCache: function (aSourceMapURL, opts = { hard: false }) {
let oldSm = this._sourceMapCache[aSourceMapURL];
if (opts.hard) {
delete this._sourceMapCache[aSourceMapURL];
}
if (oldSm) {
// Clear out the current cache so all sources will get the new one
for (let [source, sm] of this._sourceMaps.entries()) {
if (sm === oldSm) {
this._sourceMaps.delete(source);
}
}
}
},
/*
* Forcefully change the source map of a source, changing the
* sourceMapURL and installing the source map in the cache. This is
* necessary to expose changes across Debugger instances
* (pretty-printing is the use case). Generate a random url if one
* isn't specified, allowing you to set "anonymous" source maps.
*
* @param aSource Debugger.Source
* The source to change the sourceMapURL property
* @param aUrl string
* The source map URL (optional)
* @param aMap SourceMapConsumer
* The source map instance
*/
setSourceMapHard: function (aSource, aUrl, aMap) {
let url = aUrl;
if (!url) {
// This is a littly hacky, but we want to forcefully set a
// sourcemap regardless of sourcemap settings. We want to
// literally change the sourceMapURL so that all debuggers will
// get this and pretty-printing will Just Work (Debugger.Source
// instances are per-debugger, so we can't key off that). To
// avoid tons of work serializing the sourcemap into a data url,
// just make a fake URL and stick the sourcemap there.
url = "internal://sourcemap" + (this._anonSourceMapId++) + "/";
}
aSource.sourceMapURL = url;
// Forcefully set the sourcemap cache. This will be used even if
// sourcemaps are disabled.
this._sourceMapCache[url] = resolve(aMap);
this.emit("updatedSource", this.getSourceActor(aSource));
},
/**
* Return the non-source-mapped location of the given Debugger.Frame. If the
* frame does not have a script, the location's properties are all null.
*
* @param Debugger.Frame aFrame
* The frame whose location we are getting.
* @returns Object
* Returns an object of the form { source, line, column }
*/
getFrameLocation: function (aFrame) {
if (!aFrame || !aFrame.script) {
return new GeneratedLocation();
}
let {lineNumber, columnNumber} =
aFrame.script.getOffsetLocation(aFrame.offset);
return new GeneratedLocation(
this.createNonSourceMappedActor(aFrame.script.source),
lineNumber,
columnNumber
);
},
/**
* Returns a promise of the location in the original source if the source is
* source mapped, otherwise a promise of the same location. This can
* be called with a source from *any* Debugger instance and we make
* sure to that it works properly, reusing source maps if already
* fetched. Use this from any actor that needs sourcemapping.
*/
getOriginalLocation: function (generatedLocation) {
let {
generatedSourceActor,
generatedLine,
generatedColumn
} = generatedLocation;
let source = generatedSourceActor.source;
let url = source ? source.url : generatedSourceActor._originalUrl;
// In certain scenarios the source map may have not been fetched
// yet (or at least tied to this Debugger.Source instance), so use
// `fetchSourceMap` instead of `getSourceMap`. This allows this
// function to be called from anywere (across debuggers) and it
// should just automatically work.
return this.fetchSourceMap(source).then(map => {
if (map) {
let {
source: originalUrl,
line: originalLine,
column: originalColumn,
name: originalName
} = map.originalPositionFor({
line: generatedLine,
column: generatedColumn == null ? Infinity : generatedColumn
});
// Since the `Debugger.Source` instance may come from a
// different `Debugger` instance (any actor can call this
// method), we can't rely on any of the source discovery
// setup (`_discoverSources`, etc) to have been run yet. So
// we have to assume that the actor may not already exist,
// and we might need to create it, so use `source` and give
// it the required parameters for a sourcemapped source.
return new OriginalLocation(
originalUrl ? this.source({
originalUrl: originalUrl,
generatedSource: source
}) : null,
originalLine,
originalColumn,
originalName
);
}
// No source map
return OriginalLocation.fromGeneratedLocation(generatedLocation);
});
},
getAllGeneratedLocations: function (originalLocation) {
let {
originalSourceActor,
originalLine,
originalColumn
} = originalLocation;
let source = (originalSourceActor.source ||
originalSourceActor.generatedSource);
return this.fetchSourceMap(source).then((map) => {
if (map) {
map.computeColumnSpans();
return map.allGeneratedPositionsFor({
source: originalSourceActor.url,
line: originalLine,
column: originalColumn
}).map(({ line, column, lastColumn }) => {
return new GeneratedLocation(
this.createNonSourceMappedActor(source),
line,
column,
lastColumn
);
});
}
return [GeneratedLocation.fromOriginalLocation(originalLocation)];
});
},
/**
* Returns a promise of the location in the generated source corresponding to
* the original source and line given.
*
* When we pass a script S representing generated code to `sourceMap`,
* above, that returns a promise P. The process of resolving P populates
* the tables this function uses; thus, it won't know that S's original
* source URLs map to S until P is resolved.
*/
getGeneratedLocation: function (originalLocation) {
let { originalSourceActor } = originalLocation;
// Both original sources and normal sources could have sourcemaps,
// because normal sources can be pretty-printed which generates a
// sourcemap for itself. Check both of the source properties to make it work
// for both kinds of sources.
let source = originalSourceActor.source || originalSourceActor.generatedSource;
// See comment about `fetchSourceMap` in `getOriginalLocation`.
return this.fetchSourceMap(source).then((map) => {
if (map) {
let {
originalLine,
originalColumn
} = originalLocation;
let {
line: generatedLine,
column: generatedColumn
} = map.generatedPositionFor({
source: originalSourceActor.url,
line: originalLine,
column: originalColumn == null ? 0 : originalColumn,
bias: SourceMapConsumer.LEAST_UPPER_BOUND
});
return new GeneratedLocation(
this.createNonSourceMappedActor(source),
generatedLine,
generatedColumn
);
}
return GeneratedLocation.fromOriginalLocation(originalLocation);
});
},
/**
* Returns true if URL for the given source is black boxed.
*
* @param aURL String
* The URL of the source which we are checking whether it is black
* boxed or not.
*/
isBlackBoxed: function (aURL) {
return this.blackBoxedSources.has(aURL);
},
/**
* Add the given source URL to the set of sources that are black boxed.
*
* @param aURL String
* The URL of the source which we are black boxing.
*/
blackBox: function (aURL) {
this.blackBoxedSources.add(aURL);
},
/**
* Remove the given source URL to the set of sources that are black boxed.
*
* @param aURL String
* The URL of the source which we are no longer black boxing.
*/
unblackBox: function (aURL) {
this.blackBoxedSources.delete(aURL);
},
/**
* Returns true if the given URL is pretty printed.
*
* @param aURL String
* The URL of the source that might be pretty printed.
*/
isPrettyPrinted: function (aURL) {
return this.prettyPrintedSources.has(aURL);
},
/**
* Add the given URL to the set of sources that are pretty printed.
*
* @param aURL String
* The URL of the source to be pretty printed.
*/
prettyPrint: function (aURL, aIndent) {
this.prettyPrintedSources.set(aURL, aIndent);
},
/**
* Return the indent the given URL was pretty printed by.
*/
prettyPrintIndent: function (aURL) {
return this.prettyPrintedSources.get(aURL);
},
/**
* Remove the given URL from the set of sources that are pretty printed.
*
* @param aURL String
* The URL of the source that is no longer pretty printed.
*/
disablePrettyPrint: function (aURL) {
this.prettyPrintedSources.delete(aURL);
},
iter: function () {
let actors = Object.keys(this._sourceMappedSourceActors).map(k => {
return this._sourceMappedSourceActors[k];
});
for (let actor of this._sourceActors.values()) {
if (!this._sourceMaps.has(actor.source)) {
actors.push(actor);
}
}
return actors;
}
};
/*
* Checks if a source should never be displayed to the user because
* it's either internal or we don't support in the UI yet.
*/
function isHiddenSource(aSource) {
// Ignore the internal Function.prototype script
return aSource.text === "() {\n}";
}
/**
* Returns true if its argument is not null.
*/
function isNotNull(aThing) {
return aThing !== null;
}
exports.TabSources = TabSources;
exports.isHiddenSource = isHiddenSource;

View file

@ -0,0 +1,78 @@
/* -*- indent-tabs-mode: nil; 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";
var { Cu, CC, Ci, Cc } = require("chrome");
const { DebuggerServer } = require("devtools/server/main");
const promise = require("promise");
/**
* Support for actor registration. Main used by ActorRegistryActor
* for dynamic registration of new actors.
*
* @param sourceText {String} Source of the actor implementation
* @param fileName {String} URL of the actor module (for proper stack traces)
* @param options {Object} Configuration object
*/
exports.registerActor = function (sourceText, fileName, options) {
// Register in the current process
exports.registerActorInCurrentProcess(sourceText, fileName, options);
// Register in any child processes
return DebuggerServer.setupInChild({
module: "devtools/server/actors/utils/actor-registry-utils",
setupChild: "registerActorInCurrentProcess",
args: [sourceText, fileName, options],
waitForEval: true
});
};
exports.registerActorInCurrentProcess = function (sourceText, fileName, options) {
const principal = CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")();
const sandbox = Cu.Sandbox(principal);
sandbox.exports = {};
sandbox.require = require;
Cu.evalInSandbox(sourceText, sandbox, "1.8", fileName, 1);
let { prefix, constructor, type } = options;
if (type.global && !DebuggerServer.globalActorFactories.hasOwnProperty(prefix)) {
DebuggerServer.addGlobalActor({
constructorName: constructor,
constructorFun: sandbox[constructor]
}, prefix);
}
if (type.tab && !DebuggerServer.tabActorFactories.hasOwnProperty(prefix)) {
DebuggerServer.addTabActor({
constructorName: constructor,
constructorFun: sandbox[constructor]
}, prefix);
}
};
exports.unregisterActor = function (options) {
// Unregister in the current process
exports.unregisterActorInCurrentProcess(options);
// Unregister in any child processes
DebuggerServer.setupInChild({
module: "devtools/server/actors/utils/actor-registry-utils",
setupChild: "unregisterActorInCurrentProcess",
args: [options]
});
};
exports.unregisterActorInCurrentProcess = function (options) {
if (options.tab) {
DebuggerServer.removeTabActor(options);
}
if (options.global) {
DebuggerServer.removeGlobalActor(options);
}
};

View file

@ -0,0 +1,113 @@
{
"OscillatorNode": {
"source": true,
"properties": {
"type": {},
"frequency": {
"param": true
},
"detune": {
"param": true
}
}
},
"GainNode": {
"properties": { "gain": { "param": true }}
},
"DelayNode": {
"properties": { "delayTime": { "param": true }}
},
"AudioBufferSourceNode": {
"source": true,
"properties": {
"buffer": { "Buffer": true },
"playbackRate": {
"param": true
},
"loop": {},
"loopStart": {},
"loopEnd": {}
}
},
"ScriptProcessorNode": {
"properties": { "bufferSize": { "readonly": true }}
},
"PannerNode": {
"properties": {
"panningModel": {},
"distanceModel": {},
"refDistance": {},
"maxDistance": {},
"rolloffFactor": {},
"coneInnerAngle": {},
"coneOuterAngle": {},
"coneOuterGain": {}
}
},
"ConvolverNode": {
"properties": {
"buffer": { "Buffer": true },
"normalize": {}
}
},
"DynamicsCompressorNode": {
"properties": {
"threshold": { "param": true },
"knee": { "param": true },
"ratio": { "param": true },
"reduction": {},
"attack": { "param": true },
"release": { "param": true }
}
},
"BiquadFilterNode": {
"properties": {
"type": {},
"frequency": { "param": true },
"Q": { "param": true },
"detune": { "param": true },
"gain": { "param": true }
}
},
"WaveShaperNode": {
"properties": {
"curve": { "Float32Array": true },
"oversample": {}
}
},
"AnalyserNode": {
"properties": {
"fftSize": {},
"minDecibels": {},
"maxDecibels": {},
"smoothingTimeConstant": {},
"frequencyBinCount": { "readonly": true }
}
},
"AudioDestinationNode": {
"unbypassable": true
},
"ChannelSplitterNode": {
"unbypassable": true
},
"ChannelMergerNode": {
"unbypassable": true
},
"MediaElementAudioSourceNode": {
"source": true
},
"MediaStreamAudioSourceNode": {
"source": true
},
"MediaStreamAudioDestinationNode": {
"unbypassable": true,
"properties": {
"stream": { "MediaStream": true }
}
},
"StereoPannerNode": {
"properties": {
"pan": { "param": true }
}
}
}

View file

@ -0,0 +1,373 @@
/**
* web-audio-automation-timeline - 1.0.3
* https://github.com/jsantell/web-audio-automation-timeline
* MIT License, copyright (c) 2014 Jordan Santell
*/
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.Timeline=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
module.exports = require("./lib/timeline").Timeline;
},{"./lib/timeline":4}],2:[function(require,module,exports){
var F = require("./formulas");
function TimelineEvent (eventName, value, time, timeConstant, duration) {
this.type = eventName;
this.value = value;
this.time = time;
this.constant = timeConstant || 0;
this.duration = duration || 0;
}
exports.TimelineEvent = TimelineEvent;
TimelineEvent.prototype.exponentialApproach = function (lastValue, time) {
return F.exponentialApproach(this.time, lastValue, this.value, this.constant, time);
}
TimelineEvent.prototype.extractValueFromCurve = function (time) {
return F.extractValueFromCurve(this.time, this.value, this.value.length, this.duration, time);
}
TimelineEvent.prototype.linearInterpolate = function (next, time) {
return F.linearInterpolate(this.time, this.value, next.time, next.value, time);
}
TimelineEvent.prototype.exponentialInterpolate = function (next, time) {
return F.exponentialInterpolate(this.time, this.value, next.time, next.value, time);
}
},{"./formulas":3}],3:[function(require,module,exports){
var EPSILON = 0.0000000001;
exports.linearInterpolate = function (t0, v0, t1, v1, t) {
return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
};
exports.exponentialInterpolate = function (t0, v0, t1, v1, t) {
return v0 * Math.pow(v1 / v0, (t - t0) / (t1 - t0));
};
exports.extractValueFromCurve = function (start, curve, curveLength, duration, t) {
var ratio;
// If time is after duration, return the last curve value,
// or if ratio is >= 1
if (t >= start + duration || (ratio = Math.max((t - start) / duration, 0)) >= 1) {
return curve[curveLength - 1];
}
return curve[~~(curveLength * ratio)];
};
exports.exponentialApproach = function (t0, v0, v1, timeConstant, t) {
return v1 + (v0 - v1) * Math.exp(-(t - t0) / timeConstant);
};
// Since we are going to accumulate error by adding 0.01 multiple times
// in a loop, we want to fuzz the equality check in `getValueAtTime`
exports.fuzzyEqual = function (lhs, rhs) {
return Math.abs(lhs - rhs) < EPSILON;
};
exports.EPSILON = EPSILON;
},{}],4:[function(require,module,exports){
var TimelineEvent = require("./event").TimelineEvent;
var F = require("./formulas");
exports.Timeline = Timeline;
function Timeline (defaultValue) {
this.events = [];
this._value = defaultValue || 0;
}
Timeline.prototype.getEventCount = function () {
return this.events.length;
};
Timeline.prototype.value = function () {
return this._value;
};
Timeline.prototype.setValue = function (value) {
if (this.events.length === 0) {
this._value = value;
}
};
Timeline.prototype.getValue = function () {
if (this.events.length) {
throw new Error("Can only call `getValue` when there are 0 events.");
}
return this._value;
};
Timeline.prototype.getValueAtTime = function (time) {
return this._getValueAtTimeHelper(time);
};
Timeline.prototype._getValueAtTimeHelper = function (time) {
var bailOut = false;
var previous = null;
var next = null;
var lastComputedValue = null; // Used for `setTargetAtTime` nodes
var events = this.events;
var e;
for (var i = 0; !bailOut && i < events.length; i++) {
if (F.fuzzyEqual(time, events[i].time)) {
// Find the last event with the same time as `time`
do {
++i;
} while (i < events.length && F.fuzzyEqual(time, events[i].time));
e = events[i - 1];
// `setTargetAtTime` can be handled no matter what their next event is (if they have one)
if (e.type === "setTargetAtTime") {
lastComputedValue = this._lastComputedValue(e);
return e.exponentialApproach(lastComputedValue, time);
}
// `setValueCurveAtTime` events can be handled no matter what their next event node is
// (if they have one)
if (e.type === "setValueCurveAtTime") {
return e.extractValueFromCurve(time);
}
// For other event types
return e.value;
}
previous = next;
next = events[i];
if (time < events[i].time) {
bailOut = true;
}
}
// Handle the case where the time is past all of the events
if (!bailOut) {
previous = next;
next = null;
}
// Just return the default value if we did not find anything
if (!previous && !next) {
return this._value;
}
// If the requested time is before all of the existing events
if (!previous) {
return this._value;
}
// `setTargetAtTime` can be handled no matter what their next event is (if they have one)
if (previous.type === "setTargetAtTime") {
lastComputedValue = this._lastComputedValue(previous);
return previous.exponentialApproach(lastComputedValue, time);
}
// `setValueCurveAtTime` events can be handled no matter what their next event node is
// (if they have one)
if (previous.type === "setValueCurveAtTime") {
return previous.extractValueFromCurve(time);
}
if (!next) {
if (~["setValueAtTime", "linearRampToValueAtTime", "exponentialRampToValueAtTime"].indexOf(previous.type)) {
return previous.value;
}
if (previous.type === "setValueCurveAtTime") {
return previous.extractValueFromCurve(time);
}
if (previous.type === "setTargetAtTime") {
throw new Error("unreached");
}
throw new Error("unreached");
}
// Finally handle the case where we have both a previous and a next event
// First handle the case where our range ends up in a ramp event
if (next.type === "linearRampToValueAtTime") {
return previous.linearInterpolate(next, time);
} else if (next.type === "exponentialRampToValueAtTime") {
return previous.exponentialInterpolate(next, time);
}
// Now handle all other cases
if (~["setValueAtTime", "linearRampToValueAtTime", "exponentialRampToValueAtTime"].indexOf(previous.type)) {
// If the next event type is neither linear or exponential ramp,
// the value is constant.
return previous.value;
}
if (previous.type === "setValueCurveAtTime") {
return previous.extractValueFromCurve(time);
}
if (previous.type === "setTargetAtTime") {
throw new Error("unreached");
}
throw new Error("unreached");
};
Timeline.prototype._insertEvent = function (ev) {
var events = this.events;
if (ev.type === "setValueCurveAtTime") {
if (!ev.value || !ev.value.length) {
throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
}
}
if (ev.type === "setTargetAtTime") {
if (F.fuzzyEqual(ev.constant, 0)) {
throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
}
}
// Make sure that non-curve events don't fall within the duration of a
// curve event.
for (var i = 0; i < events.length; i++) {
if (events[i].type === "setValueCurveAtTime" &&
events[i].time <= ev.time &&
(events[i].time + events[i].duration) >= ev.time) {
throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
}
}
// Make sure that curve events don't fall in a range which includes other
// events.
if (ev.type === "setValueCurveAtTime") {
for (var i = 0; i < events.length; i++) {
if (events[i].time > ev.time &&
events[i].time < (ev.time + ev.duration)) {
throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
}
}
}
// Make sure that invalid values are not used for exponential curves
if (ev.type === "exponentialRampToValueAtTime") {
if (ev.value <= 0) throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
var prev = this._getPreviousEvent(ev.time);
if (prev) {
if (prev.value <= 0) {
throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
}
} else {
if (this._value <= 0) {
throw new Error("NS_ERROR_DOM_SYNTAX_ERR");
}
}
}
for (var i = 0; i < events.length; i++) {
if (ev.time === events[i].time) {
if (ev.type === events[i].type) {
// If times and types are equal, replace the event;
events[i] = ev;
} else {
// Otherwise, place the element after the last event of another type
do { i++; }
while (i < events.length && ev.type !== events[i].type && ev.time === events[i].time);
events.splice(i, 0, ev);
}
return;
}
// Otherwise, place the event right after the latest existing event
if (ev.time < events[i].time) {
events.splice(i, 0, ev);
return;
}
}
// If we couldn't find a place for the event, just append it to the list
this.events.push(ev);
};
Timeline.prototype._getPreviousEvent = function (time) {
var previous = null, next = null;
var bailOut = false;
var events = this.events;
for (var i = 0; !bailOut && i < events.length; i++) {
if (time === events[i]) {
do { ++i; }
while (i < events.length && time === events[i].time);
return events[i - 1];
}
previous = next;
next = events[i];
if (time < events[i].time) {
bailOut = true;
}
}
// Handle the case where the time is past all the events
if (!bailOut) {
previous = next;
}
return previous;
};
/**
* Calculates the previous value of the timeline, used for
* `setTargetAtTime` nodes. Takes an event, and returns
* the previous computed value for any sample taken during that
* exponential approach node.
*/
Timeline.prototype._lastComputedValue = function (event) {
// If equal times, return the value for the previous event, before
// the `setTargetAtTime` node.
var lastEvent = this._getPreviousEvent(event.time - F.EPSILON);
// If no event before the setTargetAtTime event, then return the
// intrinsic value.
if (!lastEvent) {
return this._value;
}
// Otherwise, return the value for the previous event, which should
// always be the last computed value (? I think?)
else {
return lastEvent.value;
}
};
Timeline.prototype.setValueAtTime = function (value, startTime) {
this._insertEvent(new TimelineEvent("setValueAtTime", value, startTime));
};
Timeline.prototype.linearRampToValueAtTime = function (value, endTime) {
this._insertEvent(new TimelineEvent("linearRampToValueAtTime", value, endTime));
};
Timeline.prototype.exponentialRampToValueAtTime = function (value, endTime) {
this._insertEvent(new TimelineEvent("exponentialRampToValueAtTime", value, endTime));
};
Timeline.prototype.setTargetAtTime = function (value, startTime, timeConstant) {
this._insertEvent(new TimelineEvent("setTargetAtTime", value, startTime, timeConstant));
};
Timeline.prototype.setValueCurveAtTime = function (value, startTime, duration) {
this._insertEvent(new TimelineEvent("setValueCurveAtTime", value, startTime, null, duration));
};
Timeline.prototype.cancelScheduledValues = function (time) {
for (var i = 0; i < this.events.length; i++) {
if (this.events[i].time >= time) {
this.events = this.events.slice(0, i);
break;
}
}
};
Timeline.prototype.cancelAllEvents = function () {
this.events.length = 0;
};
},{"./event":2,"./formulas":3}]},{},[1])(1)
});

View file

@ -0,0 +1,61 @@
/* 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 { Cu } = require("chrome");
/**
* Returns the grid fragment array with all the grid fragment data stringifiable.
*
* @param {Object} fragments
* Grid fragment object.
* @return {Array} representation with the grid fragment data stringifiable.
*/
function getStringifiableFragments(fragments = []) {
if (fragments[0] && Cu.isDeadWrapper(fragments[0])) {
return {};
}
return fragments.map(getStringifiableFragment);
}
/**
* Returns a string representation of the CSS Grid data as returned by
* node.getGridFragments. This is useful to compare grid state at each update and redraw
* the highlighter if needed. It also seralizes the grid fragment data so it can be used
* by protocol.js.
*
* @param {Object} fragments
* Grid fragment object.
* @return {String} representation of the CSS grid fragment data.
*/
function stringifyGridFragments(fragments) {
return JSON.stringify(getStringifiableFragments(fragments));
}
function getStringifiableFragment(fragment) {
return {
cols: getStringifiableDimension(fragment.cols),
rows: getStringifiableDimension(fragment.rows)
};
}
function getStringifiableDimension(dimension) {
return {
lines: [...dimension.lines].map(getStringifiableLine),
tracks: [...dimension.tracks].map(getStringifiableTrack),
};
}
function getStringifiableLine({ breadth, number, start, names }) {
return { breadth, number, start, names };
}
function getStringifiableTrack({ breadth, start, state, type }) {
return { breadth, start, state, type };
}
exports.getStringifiableFragments = getStringifiableFragments;
exports.stringifyGridFragments = stringifyGridFragments;

View file

@ -0,0 +1,101 @@
/* -*- 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 EventEmitter = require("devtools/shared/event-emitter");
const Debugger = require("Debugger");
const { reportException } = require("devtools/shared/DevToolsUtils");
/**
* Multiple actors that use a |Debugger| instance come in a few versions, each
* with a different set of debuggees. One version for content tabs (globals
* within a tab), one version for chrome debugging (all globals), and sometimes
* a third version for addon debugging (chrome globals the addon is loaded in
* and content globals the addon injects scripts into). The |makeDebugger|
* function helps us avoid repeating the logic for finding and maintaining the
* correct set of globals for a given |Debugger| instance across each version of
* all of our actors.
*
* The |makeDebugger| function expects a single object parameter with the
* following properties:
*
* @param Function findDebuggees
* Called with one argument: a |Debugger| instance. This function should
* return an iterable of globals to be added to the |Debugger|
* instance. The globals may be wrapped in a |Debugger.Object|, or
* unwrapped.
*
* @param Function shouldAddNewGlobalAsDebuggee
* Called with one argument: a |Debugger.Object| wrapping a global
* object. This function must return |true| if the global object should
* be added as debuggee, and |false| otherwise.
*
* @returns Debugger
* Returns a |Debugger| instance that can manage its set of debuggee
* globals itself and is decorated with the |EventEmitter| class.
*
* Events emitted by the returned |Debugger| instance:
*
* - "newGlobal": Emitted when a new global has been added as a
* debuggee. Passes the |Debugger.Object| wrapping the new
* debuggee global to listeners.
*
* Existing |Debugger| properties set on the returned |Debugger|
* instance:
*
* - onNewGlobalObject: The |Debugger| will automatically add new
* globals as debuggees if calling |shouldAddNewGlobalAsDebuggee|
* with the global returns true.
*
* - uncaughtExceptionHook: The |Debugger| already has an error
* reporter attached to |uncaughtExceptionHook|, so if any
* |Debugger| hooks fail, the error will be reported.
*
* New properties set on the returned |Debugger| instance:
*
* - addDebuggees: A function which takes no arguments. It adds all
* current globals that should be debuggees (as determined by
* |findDebuggees|) to the |Debugger| instance.
*/
module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebuggee }) {
const dbg = new Debugger();
EventEmitter.decorate(dbg);
dbg.allowUnobservedAsmJS = true;
dbg.uncaughtExceptionHook = reportDebuggerHookException;
dbg.onNewGlobalObject = function (global) {
if (shouldAddNewGlobalAsDebuggee(global)) {
safeAddDebuggee(this, global);
}
};
dbg.addDebuggees = function () {
for (let global of findDebuggees(this)) {
safeAddDebuggee(this, global);
}
};
return dbg;
};
const reportDebuggerHookException = e => reportException("Debugger Hook", e);
/**
* Add |global| as a debuggee to |dbg|, handling error cases.
*/
function safeAddDebuggee(dbg, global) {
try {
let wrappedGlobal = dbg.addDebuggee(global);
if (wrappedGlobal) {
dbg.emit("newGlobal", wrappedGlobal);
}
} catch (e) {
// Ignoring attempt to add the debugger's compartment as a debuggee.
}
}

View file

@ -0,0 +1,44 @@
/* -*- 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 DevToolsUtils = require("devtools/shared/DevToolsUtils");
const Services = require("Services");
loader.lazyServiceGetter(this, "AddonPathService",
"@mozilla.org/addon-path-service;1",
"amIAddonPathService");
const B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
const GRAPHENE_ID = "{d1bfe7d9-c01e-4237-998b-7b5f960a4314}";
/**
* This is a wrapper around amIAddonPathService.mapURIToAddonID which always returns
* false on B2G and graphene to avoid loading the add-on manager there and
* reports any exceptions rather than throwing so that the caller doesn't have
* to worry about them.
*/
if (!Services.appinfo
|| Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT
|| Services.appinfo.ID === undefined /* XPCShell */
|| Services.appinfo.ID == B2G_ID
|| Services.appinfo.ID == GRAPHENE_ID
|| !AddonPathService) {
module.exports = function mapURIToAddonId(uri) {
return false;
};
} else {
module.exports = function mapURIToAddonId(uri) {
try {
return AddonPathService.mapURIToAddonId(uri);
}
catch (e) {
DevToolsUtils.reportException("mapURIToAddonId", e);
return false;
}
};
}

View file

@ -0,0 +1,19 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'actor-registry-utils.js',
'audionodes.json',
'automation-timeline.js',
'css-grid-utils.js',
'make-debugger.js',
'map-uri-to-addon-id.js',
'stack.js',
'TabSources.js',
'walker-search.js',
'webconsole-utils.js',
'webconsole-worker-utils.js',
)

View file

@ -0,0 +1,185 @@
/* 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";
var {Class} = require("sdk/core/heritage");
/**
* A helper class that stores stack frame objects. Each frame is
* assigned an index, and if a frame is added more than once, the same
* index is used. Users of the class can get an array of all frames
* that have been added.
*/
var StackFrameCache = Class({
/**
* Initialize this object.
*/
initialize: function () {
this._framesToIndices = null;
this._framesToForms = null;
this._lastEventSize = 0;
},
/**
* Prepare to accept frames.
*/
initFrames: function () {
if (this._framesToIndices) {
// The maps are already initialized.
return;
}
this._framesToIndices = new Map();
this._framesToForms = new Map();
this._lastEventSize = 0;
},
/**
* Forget all stored frames and reset to the initialized state.
*/
clearFrames: function () {
this._framesToIndices.clear();
this._framesToIndices = null;
this._framesToForms.clear();
this._framesToForms = null;
this._lastEventSize = 0;
},
/**
* Add a frame to this stack frame cache, and return the index of
* the frame.
*/
addFrame: function (frame) {
this._assignFrameIndices(frame);
this._createFrameForms(frame);
return this._framesToIndices.get(frame);
},
/**
* A helper method for the memory actor. This populates the packet
* object with "frames" property. Each of these
* properties will be an array indexed by frame ID. "frames" will
* contain frame objects (see makeEvent).
*
* @param packet
* The packet to update.
*
* @returns packet
*/
updateFramePacket: function (packet) {
// Now that we are guaranteed to have a form for every frame, we know the
// size the "frames" property's array must be. We use that information to
// create dense arrays even though we populate them out of order.
const size = this._framesToForms.size;
packet.frames = Array(size).fill(null);
// Populate the "frames" properties.
for (let [stack, index] of this._framesToIndices) {
packet.frames[index] = this._framesToForms.get(stack);
}
return packet;
},
/**
* If any new stack frames have been added to this cache since the
* last call to makeEvent (clearing the cache also resets the "last
* call"), then return a new array describing the new frames. If no
* new frames are available, return null.
*
* The frame cache assumes that the user of the cache keeps track of
* all previously-returned arrays and, in theory, concatenates them
* all to form a single array holding all frames added to the cache
* since the last reset. This concatenated array can be indexed by
* the frame ID. The array returned by this function, though, is
* dense and starts at 0.
*
* Each element in the array is an object of the form:
* {
* line: <line number for this frame>,
* column: <column number for this frame>,
* source: <filename string for this frame>,
* functionDisplayName: <this frame's inferred function name function or null>,
* parent: <frame ID -- an index into the concatenated array mentioned above>
* asyncCause: the async cause, or null
* asyncParent: <frame ID -- an index into the concatenated array mentioned above>
* }
*
* The intent of this approach is to make it simpler to efficiently
* send frame information over the debugging protocol, by only
* sending new frames.
*
* @returns array or null
*/
makeEvent: function () {
const size = this._framesToForms.size;
if (!size || size <= this._lastEventSize) {
return null;
}
let packet = Array(size - this._lastEventSize).fill(null);
for (let [stack, index] of this._framesToIndices) {
if (index >= this._lastEventSize) {
packet[index - this._lastEventSize] = this._framesToForms.get(stack);
}
}
this._lastEventSize = size;
return packet;
},
/**
* Assigns an index to the given frame and its parents, if an index is not
* already assigned.
*
* @param SavedFrame frame
* A frame to assign an index to.
*/
_assignFrameIndices: function (frame) {
if (this._framesToIndices.has(frame)) {
return;
}
if (frame) {
this._assignFrameIndices(frame.parent);
this._assignFrameIndices(frame.asyncParent);
}
const index = this._framesToIndices.size;
this._framesToIndices.set(frame, index);
},
/**
* Create the form for the given frame, if one doesn't already exist.
*
* @param SavedFrame frame
* A frame to create a form for.
*/
_createFrameForms: function (frame) {
if (this._framesToForms.has(frame)) {
return;
}
let form = null;
if (frame) {
form = {
line: frame.line,
column: frame.column,
source: frame.source,
functionDisplayName: frame.functionDisplayName,
parent: this._framesToIndices.get(frame.parent),
asyncParent: this._framesToIndices.get(frame.asyncParent),
asyncCause: frame.asyncCause
};
this._createFrameForms(frame.parent);
this._createFrameForms(frame.asyncParent);
}
this._framesToForms.set(frame, form);
},
});
exports.StackFrameCache = StackFrameCache;

View file

@ -0,0 +1,278 @@
/* 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";
/**
* The walker-search module provides a simple API to index and search strings
* and elements inside a given document.
* It indexes tag names, attribute names and values, and text contents.
* It provides a simple search function that returns a list of nodes that
* matched.
*/
const {Ci, Cu} = require("chrome");
/**
* The WalkerIndex class indexes the document (and all subdocs) from
* a given walker.
*
* It is only indexed the first time the data is accessed and will be
* re-indexed if a mutation happens between requests.
*
* @param {Walker} walker The walker to be indexed
*/
function WalkerIndex(walker) {
this.walker = walker;
this.clearIndex = this.clearIndex.bind(this);
// Kill the index when mutations occur, the next data get will re-index.
this.walker.on("any-mutation", this.clearIndex);
}
WalkerIndex.prototype = {
/**
* Destroy this instance, releasing all data and references
*/
destroy: function () {
this.walker.off("any-mutation", this.clearIndex);
},
clearIndex: function () {
if (!this.currentlyIndexing) {
this._data = null;
}
},
get doc() {
return this.walker.rootDoc;
},
/**
* Get the indexed data
* This getter also indexes if it hasn't been done yet or if the state is
* dirty
*
* @returns Map<String, Array<{type:String, node:DOMNode}>>
* A Map keyed on the searchable value, containing an array with
* objects containing the 'type' (one of ALL_RESULTS_TYPES), and
* the DOM Node.
*/
get data() {
if (!this._data) {
this._data = new Map();
this.index();
}
return this._data;
},
_addToIndex: function (type, node, value) {
// Add an entry for this value if there isn't one
let entry = this._data.get(value);
if (!entry) {
this._data.set(value, []);
}
// Add the type/node to the list
this._data.get(value).push({
type: type,
node: node
});
},
index: function () {
// Handle case where iterating nextNode() with the deepTreeWalker triggers
// a mutation (Bug 1222558)
this.currentlyIndexing = true;
let documentWalker = this.walker.getDocumentWalker(this.doc);
while (documentWalker.nextNode()) {
let node = documentWalker.currentNode;
if (node.nodeType === 1) {
// For each element node, we get the tagname and all attributes names
// and values
let localName = node.localName;
if (localName === "_moz_generated_content_before") {
this._addToIndex("tag", node, "::before");
this._addToIndex("text", node, node.textContent.trim());
} else if (localName === "_moz_generated_content_after") {
this._addToIndex("tag", node, "::after");
this._addToIndex("text", node, node.textContent.trim());
} else {
this._addToIndex("tag", node, node.localName);
}
for (let {name, value} of node.attributes) {
this._addToIndex("attributeName", node, name);
this._addToIndex("attributeValue", node, value);
}
} else if (node.textContent && node.textContent.trim().length) {
// For comments and text nodes, we get the text
this._addToIndex("text", node, node.textContent.trim());
}
}
this.currentlyIndexing = false;
}
};
exports.WalkerIndex = WalkerIndex;
/**
* The WalkerSearch class provides a way to search an indexed document as well
* as find elements that match a given css selector.
*
* Usage example:
* let s = new WalkerSearch(doc);
* let res = s.search("lang", index);
* for (let {matched, results} of res) {
* for (let {node, type} of results) {
* console.log("The query matched a node's " + type);
* console.log("Node that matched", node);
* }
* }
* s.destroy();
*
* @param {Walker} the walker to be searched
*/
function WalkerSearch(walker) {
this.walker = walker;
this.index = new WalkerIndex(this.walker);
}
WalkerSearch.prototype = {
destroy: function () {
this.index.destroy();
this.walker = null;
},
_addResult: function (node, type, results) {
if (!results.has(node)) {
results.set(node, []);
}
let matches = results.get(node);
// Do not add if the exact same result is already in the list
let isKnown = false;
for (let match of matches) {
if (match.type === type) {
isKnown = true;
break;
}
}
if (!isKnown) {
matches.push({type});
}
},
_searchIndex: function (query, options, results) {
for (let [matched, res] of this.index.data) {
if (!options.searchMethod(query, matched)) {
continue;
}
// Add any relevant results (skipping non-requested options).
res.filter(entry => {
return options.types.indexOf(entry.type) !== -1;
}).forEach(({node, type}) => {
this._addResult(node, type, results);
});
}
},
_searchSelectors: function (query, options, results) {
// If the query is just one "word", no need to search because _searchIndex
// will lead the same results since it has access to tagnames anyway
let isSelector = query && query.match(/[ >~.#\[\]]/);
if (options.types.indexOf("selector") === -1 || !isSelector) {
return;
}
let nodes = this.walker._multiFrameQuerySelectorAll(query);
for (let node of nodes) {
this._addResult(node, "selector", results);
}
},
/**
* Search the document
* @param {String} query What to search for
* @param {Object} options The following options are accepted:
* - searchMethod {String} one of WalkerSearch.SEARCH_METHOD_*
* defaults to WalkerSearch.SEARCH_METHOD_CONTAINS (does not apply to
* selector search type)
* - types {Array} a list of things to search for (tag, text, attributes, etc)
* defaults to WalkerSearch.ALL_RESULTS_TYPES
* @return {Array} An array is returned with each item being an object like:
* {
* node: <the dom node that matched>,
* type: <the type of match: one of WalkerSearch.ALL_RESULTS_TYPES>
* }
*/
search: function (query, options = {}) {
options.searchMethod = options.searchMethod || WalkerSearch.SEARCH_METHOD_CONTAINS;
options.types = options.types || WalkerSearch.ALL_RESULTS_TYPES;
// Empty strings will return no results, as will non-string input
if (typeof query !== "string") {
query = "";
}
// Store results in a map indexed by nodes to avoid duplicate results
let results = new Map();
// Search through the indexed data
this._searchIndex(query, options, results);
// Search with querySelectorAll
this._searchSelectors(query, options, results);
// Concatenate all results into an Array to return
let resultList = [];
for (let [node, matches] of results) {
for (let {type} of matches) {
resultList.push({
node: node,
type: type,
});
// For now, just do one result per node since the frontend
// doesn't have a way to highlight each result individually
// yet.
break;
}
}
let documents = this.walker.tabActor.windows.map(win=>win.document);
// Sort the resulting nodes by order of appearance in the DOM
resultList.sort((a, b) => {
// Disconnected nodes won't get good results from compareDocumentPosition
// so check the order of their document instead.
if (a.node.ownerDocument != b.node.ownerDocument) {
let indA = documents.indexOf(a.node.ownerDocument);
let indB = documents.indexOf(b.node.ownerDocument);
return indA - indB;
}
// If the same document, then sort on DOCUMENT_POSITION_FOLLOWING (4)
// which means B is after A.
return a.node.compareDocumentPosition(b.node) & 4 ? -1 : 1;
});
return resultList;
}
};
WalkerSearch.SEARCH_METHOD_CONTAINS = (query, candidate) => {
return query && candidate.toLowerCase().indexOf(query.toLowerCase()) !== -1;
};
WalkerSearch.ALL_RESULTS_TYPES = ["tag", "text", "attributeName",
"attributeValue", "selector"];
exports.WalkerSearch = WalkerSearch;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
/* -*- indent-tabs-mode: nil; 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";
// XXXworkers This file is loaded on the server side for worker debugging.
// Since the server is running in the worker thread, it doesn't
// have access to Services / Components. This functionality
// is stubbed out to prevent errors, and will need to implemented
// for Bug 1209353.
exports.Utils = { L10n: function () {} };
exports.ConsoleServiceListener = function () {};
exports.ConsoleAPIListener = function () {};
exports.addWebConsoleCommands = function () {};
exports.ConsoleReflowListener = function () {};
exports.CONSOLE_WORKER_IDS = [];