diff options
Diffstat (limited to 'devtools/shared/fronts')
35 files changed, 3876 insertions, 0 deletions
diff --git a/devtools/shared/fronts/actor-registry.js b/devtools/shared/fronts/actor-registry.js new file mode 100644 index 0000000000..40f87b6093 --- /dev/null +++ b/devtools/shared/fronts/actor-registry.js @@ -0,0 +1,67 @@ +/* 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 { components } = require("chrome"); +const Services = require("Services"); +const { actorActorSpec, actorRegistrySpec } = require("devtools/shared/specs/actor-registry"); +const protocol = require("devtools/shared/protocol"); +const { custom } = protocol; + +loader.lazyImporter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm"); + +const ActorActorFront = protocol.FrontClassWithSpec(actorActorSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + +exports.ActorActorFront = ActorActorFront; + +function request(uri) { + return new Promise((resolve, reject) => { + try { + uri = Services.io.newURI(uri, null, null); + } catch (e) { + reject(e); + } + + NetUtil.asyncFetch({ + uri, + loadUsingSystemPrincipal: true, + }, (stream, status, req) => { + if (!components.isSuccessCode(status)) { + reject(new Error("Request failed with status code = " + + status + + " after NetUtil.asyncFetch for url = " + + uri)); + return; + } + + let source = NetUtil.readInputStreamToString(stream, stream.available()); + stream.close(); + resolve(source); + }); + }); +} + +const ActorRegistryFront = protocol.FrontClassWithSpec(actorRegistrySpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, + { actor: form.actorRegistryActor }); + + this.manage(this); + }, + + registerActor: custom(function (uri, options) { + return request(uri, options) + .then(sourceText => { + return this._registerActor(sourceText, uri, options); + }); + }, { + impl: "_registerActor" + }) +}); + +exports.ActorRegistryFront = ActorRegistryFront; diff --git a/devtools/shared/fronts/addons.js b/devtools/shared/fronts/addons.js new file mode 100644 index 0000000000..780cc9e031 --- /dev/null +++ b/devtools/shared/fronts/addons.js @@ -0,0 +1,17 @@ +/* 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 {addonsSpec} = require("devtools/shared/specs/addons"); +const protocol = require("devtools/shared/protocol"); + +const AddonsFront = protocol.FrontClassWithSpec(addonsSpec, { + initialize: function (client, {addonsActor}) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = addonsActor; + this.manage(this); + } +}); + +exports.AddonsFront = AddonsFront; diff --git a/devtools/shared/fronts/animation.js b/devtools/shared/fronts/animation.js new file mode 100644 index 0000000000..01c9f0bfab --- /dev/null +++ b/devtools/shared/fronts/animation.js @@ -0,0 +1,140 @@ +/* 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 { + Front, + FrontClassWithSpec, + custom, + preEvent +} = require("devtools/shared/protocol"); +const { + animationPlayerSpec, + animationsSpec +} = require("devtools/shared/specs/animation"); +const { Task } = require("devtools/shared/task"); + +const AnimationPlayerFront = FrontClassWithSpec(animationPlayerSpec, { + initialize: function (conn, form, detail, ctx) { + Front.prototype.initialize.call(this, conn, form, detail, ctx); + + this.state = {}; + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this._form = form; + this.state = this.initialState; + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + /** + * If the AnimationsActor was given a reference to the WalkerActor previously + * then calling this getter will return the animation target NodeFront. + */ + get animationTargetNodeFront() { + if (!this._form.animationTargetNodeActorID) { + return null; + } + + return this.conn.getActor(this._form.animationTargetNodeActorID); + }, + + /** + * Getter for the initial state of the player. Up to date states can be + * retrieved by calling the getCurrentState method. + */ + get initialState() { + return { + type: this._form.type, + startTime: this._form.startTime, + previousStartTime: this._form.previousStartTime, + currentTime: this._form.currentTime, + playState: this._form.playState, + playbackRate: this._form.playbackRate, + name: this._form.name, + duration: this._form.duration, + delay: this._form.delay, + endDelay: this._form.endDelay, + iterationCount: this._form.iterationCount, + iterationStart: this._form.iterationStart, + easing: this._form.easing, + fill: this._form.fill, + direction: this._form.direction, + isRunningOnCompositor: this._form.isRunningOnCompositor, + propertyState: this._form.propertyState, + documentCurrentTime: this._form.documentCurrentTime + }; + }, + + /** + * Executed when the AnimationPlayerActor emits a "changed" event. Used to + * update the local knowledge of the state. + */ + onChanged: preEvent("changed", function (partialState) { + let {state} = this.reconstructState(partialState); + this.state = state; + }), + + /** + * Refresh the current state of this animation on the client from information + * found on the server. Doesn't return anything, just stores the new state. + */ + refreshState: Task.async(function* () { + let data = yield this.getCurrentState(); + if (this.currentStateHasChanged) { + this.state = data; + } + }), + + /** + * getCurrentState interceptor re-constructs incomplete states since the actor + * only sends the values that have changed. + */ + getCurrentState: custom(function () { + this.currentStateHasChanged = false; + return this._getCurrentState().then(partialData => { + let {state, hasChanged} = this.reconstructState(partialData); + this.currentStateHasChanged = hasChanged; + return state; + }); + }, { + impl: "_getCurrentState" + }), + + reconstructState: function (data) { + let hasChanged = false; + + for (let key in this.state) { + if (typeof data[key] === "undefined") { + data[key] = this.state[key]; + } else if (data[key] !== this.state[key]) { + hasChanged = true; + } + } + + return {state: data, hasChanged}; + } +}); + +exports.AnimationPlayerFront = AnimationPlayerFront; + +const AnimationsFront = FrontClassWithSpec(animationsSpec, { + initialize: function (client, {animationsActor}) { + Front.prototype.initialize.call(this, client, {actor: animationsActor}); + this.manage(this); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + } +}); + +exports.AnimationsFront = AnimationsFront; diff --git a/devtools/shared/fronts/call-watcher.js b/devtools/shared/fronts/call-watcher.js new file mode 100644 index 0000000000..5f41c2fbd9 --- /dev/null +++ b/devtools/shared/fronts/call-watcher.js @@ -0,0 +1,226 @@ +/* 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 { functionCallSpec, callWatcherSpec } = require("devtools/shared/specs/call-watcher"); +const protocol = require("devtools/shared/protocol"); + +/** + * The corresponding Front object for the FunctionCallActor. + */ +const FunctionCallFront = protocol.FrontClassWithSpec(functionCallSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + }, + + /** + * Adds some generic information directly to this instance, + * to avoid extra roundtrips. + */ + form: function (form) { + this.actorID = form.actor; + this.type = form.type; + this.name = form.name; + this.file = form.file; + this.line = form.line; + this.timestamp = form.timestamp; + this.callerPreview = form.callerPreview; + this.argsPreview = form.argsPreview; + this.resultPreview = form.resultPreview; + } +}); + +exports.FunctionCallFront = FunctionCallFront; + +/** + * The corresponding Front object for the CallWatcherActor. + */ +var CallWatcherFront = +exports.CallWatcherFront = +protocol.FrontClassWithSpec(callWatcherSpec, { + initialize: function (client, { callWatcherActor }) { + protocol.Front.prototype.initialize.call(this, client, { actor: callWatcherActor }); + this.manage(this); + } +}); + +/** + * Constants. + */ +CallWatcherFront.METHOD_FUNCTION = 0; +CallWatcherFront.GETTER_FUNCTION = 1; +CallWatcherFront.SETTER_FUNCTION = 2; + +CallWatcherFront.KNOWN_METHODS = {}; + +CallWatcherFront.KNOWN_METHODS.CanvasRenderingContext2D = { + asyncDrawXULElement: { + enums: new Set([6]), + }, + drawWindow: { + enums: new Set([6]) + }, +}; + +CallWatcherFront.KNOWN_METHODS.WebGLRenderingContext = { + activeTexture: { + enums: new Set([0]), + }, + bindBuffer: { + enums: new Set([0]), + }, + bindFramebuffer: { + enums: new Set([0]), + }, + bindRenderbuffer: { + enums: new Set([0]), + }, + bindTexture: { + enums: new Set([0]), + }, + blendEquation: { + enums: new Set([0]), + }, + blendEquationSeparate: { + enums: new Set([0, 1]), + }, + blendFunc: { + enums: new Set([0, 1]), + }, + blendFuncSeparate: { + enums: new Set([0, 1, 2, 3]), + }, + bufferData: { + enums: new Set([0, 1, 2]), + }, + bufferSubData: { + enums: new Set([0, 1]), + }, + checkFramebufferStatus: { + enums: new Set([0]), + }, + clear: { + enums: new Set([0]), + }, + compressedTexImage2D: { + enums: new Set([0, 2]), + }, + compressedTexSubImage2D: { + enums: new Set([0, 6]), + }, + copyTexImage2D: { + enums: new Set([0, 2]), + }, + copyTexSubImage2D: { + enums: new Set([0]), + }, + createShader: { + enums: new Set([0]), + }, + cullFace: { + enums: new Set([0]), + }, + depthFunc: { + enums: new Set([0]), + }, + disable: { + enums: new Set([0]), + }, + drawArrays: { + enums: new Set([0]), + }, + drawElements: { + enums: new Set([0, 2]), + }, + enable: { + enums: new Set([0]), + }, + framebufferRenderbuffer: { + enums: new Set([0, 1, 2]), + }, + framebufferTexture2D: { + enums: new Set([0, 1, 2]), + }, + frontFace: { + enums: new Set([0]), + }, + generateMipmap: { + enums: new Set([0]), + }, + getBufferParameter: { + enums: new Set([0, 1]), + }, + getParameter: { + enums: new Set([0]), + }, + getFramebufferAttachmentParameter: { + enums: new Set([0, 1, 2]), + }, + getProgramParameter: { + enums: new Set([1]), + }, + getRenderbufferParameter: { + enums: new Set([0, 1]), + }, + getShaderParameter: { + enums: new Set([1]), + }, + getShaderPrecisionFormat: { + enums: new Set([0, 1]), + }, + getTexParameter: { + enums: new Set([0, 1]), + }, + getVertexAttrib: { + enums: new Set([1]), + }, + getVertexAttribOffset: { + enums: new Set([1]), + }, + hint: { + enums: new Set([0, 1]), + }, + isEnabled: { + enums: new Set([0]), + }, + pixelStorei: { + enums: new Set([0]), + }, + readPixels: { + enums: new Set([4, 5]), + }, + renderbufferStorage: { + enums: new Set([0, 1]), + }, + stencilFunc: { + enums: new Set([0]), + }, + stencilFuncSeparate: { + enums: new Set([0, 1]), + }, + stencilMaskSeparate: { + enums: new Set([0]), + }, + stencilOp: { + enums: new Set([0, 1, 2]), + }, + stencilOpSeparate: { + enums: new Set([0, 1, 2, 3]), + }, + texImage2D: { + enums: args => args.length > 6 ? new Set([0, 2, 6, 7]) : new Set([0, 2, 3, 4]), + }, + texParameterf: { + enums: new Set([0, 1]), + }, + texParameteri: { + enums: new Set([0, 1, 2]), + }, + texSubImage2D: { + enums: args => args.length === 9 ? new Set([0, 6, 7]) : new Set([0, 4, 5]), + }, + vertexAttribPointer: { + enums: new Set([2]) + }, +}; diff --git a/devtools/shared/fronts/canvas.js b/devtools/shared/fronts/canvas.js new file mode 100644 index 0000000000..f3a1a60755 --- /dev/null +++ b/devtools/shared/fronts/canvas.js @@ -0,0 +1,91 @@ +/* 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 { + frameSnapshotSpec, + canvasSpec, + CANVAS_CONTEXTS, + ANIMATION_GENERATORS, + LOOP_GENERATORS, + DRAW_CALLS, + INTERESTING_CALLS, +} = require("devtools/shared/specs/canvas"); +const protocol = require("devtools/shared/protocol"); +const promise = require("promise"); + +/** + * The corresponding Front object for the FrameSnapshotActor. + */ +const FrameSnapshotFront = protocol.FrontClassWithSpec(frameSnapshotSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + this._animationFrameEndScreenshot = null; + this._cachedScreenshots = new WeakMap(); + }, + + /** + * This implementation caches the animation frame end screenshot to optimize + * frontend requests to `generateScreenshotFor`. + */ + getOverview: protocol.custom(function () { + return this._getOverview().then(data => { + this._animationFrameEndScreenshot = data.screenshot; + return data; + }); + }, { + impl: "_getOverview" + }), + + /** + * This implementation saves a roundtrip to the backend if the screenshot + * was already generated and retrieved once. + */ + generateScreenshotFor: protocol.custom(function (functionCall) { + if (CanvasFront.ANIMATION_GENERATORS.has(functionCall.name) || + CanvasFront.LOOP_GENERATORS.has(functionCall.name)) { + return promise.resolve(this._animationFrameEndScreenshot); + } + let cachedScreenshot = this._cachedScreenshots.get(functionCall); + if (cachedScreenshot) { + return cachedScreenshot; + } + let screenshot = this._generateScreenshotFor(functionCall); + this._cachedScreenshots.set(functionCall, screenshot); + return screenshot; + }, { + impl: "_generateScreenshotFor" + }) +}); + +exports.FrameSnapshotFront = FrameSnapshotFront; + +/** + * The corresponding Front object for the CanvasActor. + */ +const CanvasFront = protocol.FrontClassWithSpec(canvasSpec, { + initialize: function (client, { canvasActor }) { + protocol.Front.prototype.initialize.call(this, client, { actor: canvasActor }); + this.manage(this); + } +}); + +/** + * Constants. + */ +CanvasFront.CANVAS_CONTEXTS = new Set(CANVAS_CONTEXTS); +CanvasFront.ANIMATION_GENERATORS = new Set(ANIMATION_GENERATORS); +CanvasFront.LOOP_GENERATORS = new Set(LOOP_GENERATORS); +CanvasFront.DRAW_CALLS = new Set(DRAW_CALLS); +CanvasFront.INTERESTING_CALLS = new Set(INTERESTING_CALLS); +CanvasFront.THUMBNAIL_SIZE = 50; +CanvasFront.WEBGL_SCREENSHOT_MAX_HEIGHT = 256; +CanvasFront.INVALID_SNAPSHOT_IMAGE = { + index: -1, + width: 0, + height: 0, + pixels: [] +}; + +exports.CanvasFront = CanvasFront; diff --git a/devtools/shared/fronts/css-properties.js b/devtools/shared/fronts/css-properties.js new file mode 100644 index 0000000000..9b3172a220 --- /dev/null +++ b/devtools/shared/fronts/css-properties.js @@ -0,0 +1,323 @@ +/* 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 { FrontClassWithSpec, Front } = require("devtools/shared/protocol"); +const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties"); +const { Task } = require("devtools/shared/task"); +const { CSS_PROPERTIES_DB } = require("devtools/shared/css/properties-db"); +const { cssColors } = require("devtools/shared/css/color-db"); + +/** + * Build up a regular expression that matches a CSS variable token. This is an + * ident token that starts with two dashes "--". + * + * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram + */ +var NON_ASCII = "[^\\x00-\\x7F]"; +var ESCAPE = "\\\\[^\n\r]"; +var FIRST_CHAR = ["[_a-z]", NON_ASCII, ESCAPE].join("|"); +var TRAILING_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|"); +var IS_VARIABLE_TOKEN = new RegExp(`^--(${FIRST_CHAR})(${TRAILING_CHAR})*$`, + "i"); +/** + * Check that this is a CSS variable. + * + * @param {String} input + * @return {Boolean} + */ +function isCssVariable(input) { + return !!input.match(IS_VARIABLE_TOKEN); +} + +var cachedCssProperties = new WeakMap(); + +/** + * The CssProperties front provides a mechanism to have a one-time asynchronous + * load of a CSS properties database. This is then fed into the CssProperties + * interface that provides synchronous methods for finding out what CSS + * properties the current server supports. + */ +const CssPropertiesFront = FrontClassWithSpec(cssPropertiesSpec, { + initialize: function (client, { cssPropertiesActor }) { + Front.prototype.initialize.call(this, client, {actor: cssPropertiesActor}); + this.manage(this); + } +}); + +/** + * Ask questions to a CSS database. This class does not care how the database + * gets loaded in, only the questions that you can ask to it. + * Prototype functions are bound to 'this' so they can be passed around as helper + * functions. + * + * @param {Object} db + * A database of CSS properties + * @param {Object} inheritedList + * The key is the property name, the value is whether or not + * that property is inherited. + */ +function CssProperties(db) { + this.properties = db.properties; + this.pseudoElements = db.pseudoElements; + + this.isKnown = this.isKnown.bind(this); + this.isInherited = this.isInherited.bind(this); + this.supportsType = this.supportsType.bind(this); + this.isValidOnClient = this.isValidOnClient.bind(this); + + // A weakly held dummy HTMLDivElement to test CSS properties on the client. + this._dummyElements = new WeakMap(); +} + +CssProperties.prototype = { + /** + * Checks to see if the property is known by the browser. This function has + * `this` already bound so that it can be passed around by reference. + * + * @param {String} property The property name to be checked. + * @return {Boolean} + */ + isKnown(property) { + return !!this.properties[property] || isCssVariable(property); + }, + + /** + * Quickly check if a CSS name/value combo is valid on the client. + * + * @param {String} Property name. + * @param {String} Property value. + * @param {Document} The client's document object. + * @return {Boolean} + */ + isValidOnClient(name, value, doc) { + let dummyElement = this._dummyElements.get(doc); + if (!dummyElement) { + dummyElement = doc.createElement("div"); + this._dummyElements.set(doc, dummyElement); + } + + // `!important` is not a valid value when setting a style declaration in the + // CSS Object Model. + const sanitizedValue = ("" + value).replace(/!\s*important\s*$/, ""); + + // Test the style on the element. + dummyElement.style[name] = sanitizedValue; + const isValid = !!dummyElement.style[name]; + + // Reset the state of the dummy element; + dummyElement.style[name] = ""; + return isValid; + }, + + /** + * Get a function that will check the validity of css name/values for a given document. + * Useful for injecting isValidOnClient into components when needed. + * + * @param {Document} The client's document object. + * @return {Function} this.isValidOnClient with the document pre-set. + */ + getValidityChecker(doc) { + return (name, value) => this.isValidOnClient(name, value, doc); + }, + + /** + * Checks to see if the property is an inherited one. + * + * @param {String} property The property name to be checked. + * @return {Boolean} + */ + isInherited(property) { + return this.properties[property] && this.properties[property].isInherited; + }, + + /** + * Checks if the property supports the given CSS type. + * CSS types should come from devtools/shared/css/properties-db.js' CSS_TYPES. + * + * @param {String} property The property to be checked. + * @param {Number} type One of the type values from CSS_TYPES. + * @return {Boolean} + */ + supportsType(property, type) { + return this.properties[property] && this.properties[property].supports.includes(type); + }, + + /** + * Gets the CSS values for a given property name. + * + * @param {String} property The property to use. + * @return {Array} An array of strings. + */ + getValues(property) { + return this.properties[property] ? this.properties[property].values : []; + }, + + /** + * Gets the CSS property names. + * + * @return {Array} An array of strings. + */ + getNames(property) { + return Object.keys(this.properties); + }, + + /** + * Return a list of subproperties for the given property. If |name| + * does not name a valid property, an empty array is returned. If + * the property is not a shorthand property, then array containing + * just the property itself is returned. + * + * @param {String} name The property to query + * @return {Array} An array of subproperty names. + */ + getSubproperties(name) { + if (this.isKnown(name)) { + if (this.properties[name] && this.properties[name].subproperties) { + return this.properties[name].subproperties; + } + return [name]; + } + return []; + }, +}; + +/** + * Create a CssProperties object with a fully loaded CSS database. The + * CssProperties interface can be queried synchronously, but the initialization + * is potentially async and should be handled up-front when the tool is created. + * + * The front is returned only with this function so that it can be destroyed + * once the toolbox is destroyed. + * + * @param {Toolbox} The current toolbox. + * @returns {Promise} Resolves to {cssProperties, cssPropertiesFront}. + */ +const initCssProperties = Task.async(function* (toolbox) { + const client = toolbox.target.client; + if (cachedCssProperties.has(client)) { + return cachedCssProperties.get(client); + } + + let db, front; + + // Get the list dynamically if the cssProperties actor exists. + if (toolbox.target.hasActor("cssProperties")) { + front = CssPropertiesFront(client, toolbox.target.form); + const serverDB = yield front.getCSSDatabase(); + + // Ensure the database was returned in a format that is understood. + // Older versions of the protocol could return a blank database. + if (!serverDB.properties && !serverDB.margin) { + db = CSS_PROPERTIES_DB; + } else { + db = serverDB; + } + } else { + // The target does not support this actor, so require a static list of supported + // properties. + db = CSS_PROPERTIES_DB; + } + + const cssProperties = new CssProperties(normalizeCssData(db)); + cachedCssProperties.set(client, {cssProperties, front}); + return {cssProperties, front}; +}); + +/** + * Synchronously get a cached and initialized CssProperties. + * + * @param {Toolbox} The current toolbox. + * @returns {CssProperties} + */ +function getCssProperties(toolbox) { + if (!cachedCssProperties.has(toolbox.target.client)) { + throw new Error("The CSS database has not been initialized, please make " + + "sure initCssDatabase was called once before for this " + + "toolbox."); + } + return cachedCssProperties.get(toolbox.target.client).cssProperties; +} + +/** + * Get a client-side CssProperties. This is useful for dependencies in tests, or parts + * of the codebase that don't particularly need to match every known CSS property on + * the target. + * @return {CssProperties} + */ +function getClientCssProperties() { + return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB)); +} + +/** + * Even if the target has the cssProperties actor, the returned data may not be in the + * same shape or have all of the data we need. This normalizes the data and fills in + * any missing information like color values. + * + * @return {Object} The normalized CSS database. + */ +function normalizeCssData(db) { + if (db !== CSS_PROPERTIES_DB) { + // Firefox 49's getCSSDatabase() just returned the properties object, but + // now it returns an object with multiple types of CSS information. + if (!db.properties) { + db = { properties: db }; + } + + // Fill in any missing DB information from the static database. + db = Object.assign({}, CSS_PROPERTIES_DB, db); + + for (let name in db.properties) { + // Skip the current property if we can't find it in CSS_PROPERTIES_DB. + if (typeof CSS_PROPERTIES_DB.properties[name] !== "object") { + continue; + } + + // Add "supports" information to the css properties if it's missing. + if (!db.properties.color.supports) { + db.properties[name].supports = CSS_PROPERTIES_DB.properties[name].supports; + } + // Add "values" information to the css properties if it's missing. + if (!db.properties.color.values) { + db.properties[name].values = CSS_PROPERTIES_DB.properties[name].values; + } + // Add "subproperties" information to the css properties if it's missing. + if (!db.properties.background.subproperties) { + db.properties[name].subproperties = + CSS_PROPERTIES_DB.properties[name].subproperties; + } + } + } + + reattachCssColorValues(db); + + return db; +} + +/** + * Color values are omitted to save on space. Add them back here. + * @param {Object} The CSS database. + */ +function reattachCssColorValues(db) { + if (db.properties.color.values[0] === "COLOR") { + const colors = Object.keys(cssColors); + + for (let name in db.properties) { + const property = db.properties[name]; + // "values" can be undefined if {name} was not found in CSS_PROPERTIES_DB. + if (property.values && property.values[0] === "COLOR") { + property.values.shift(); + property.values = property.values.concat(colors).sort(); + } + } + } +} + +module.exports = { + CssPropertiesFront, + CssProperties, + getCssProperties, + getClientCssProperties, + initCssProperties +}; diff --git a/devtools/shared/fronts/csscoverage.js b/devtools/shared/fronts/csscoverage.js new file mode 100644 index 0000000000..28ab399c5e --- /dev/null +++ b/devtools/shared/fronts/csscoverage.js @@ -0,0 +1,125 @@ +/* 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 {cssUsageSpec} = require("devtools/shared/specs/csscoverage"); +const protocol = require("devtools/shared/protocol"); +const {custom} = protocol; + +const {LocalizationHelper} = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper("devtools/shared/locales/csscoverage.properties"); + +loader.lazyRequireGetter(this, "gDevTools", "devtools/client/framework/devtools", true); + +/** + * Allow: let foo = l10n.lookup("csscoverageFoo"); + */ +const l10n = exports.l10n = { + lookup: (msg) => L10N.getStr(msg) +}; + +/** + * Running more than one usage report at a time is probably bad for performance + * and it isn't particularly useful, and it's confusing from a notification POV + * so we only allow one. + */ +var isRunning = false; +var notification; +var target; +var chromeWindow; + +/** + * Front for CSSUsageActor + */ +const CSSUsageFront = protocol.FrontClassWithSpec(cssUsageSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + this.actorID = form.cssUsageActor; + this.manage(this); + }, + + _onStateChange: protocol.preEvent("state-change", function (ev) { + isRunning = ev.isRunning; + ev.target = target; + + if (isRunning) { + let gnb = chromeWindow.document.getElementById("global-notificationbox"); + notification = gnb.getNotificationWithValue("csscoverage-running"); + + if (notification == null) { + let notifyStop = reason => { + if (reason == "removed") { + this.stop(); + } + }; + + let msg = l10n.lookup("csscoverageRunningReply"); + notification = gnb.appendNotification(msg, "csscoverage-running", + "", + gnb.PRIORITY_INFO_HIGH, + null, + notifyStop); + } + } else { + if (notification) { + notification.remove(); + notification = undefined; + } + + gDevTools.showToolbox(target, "styleeditor"); + target = undefined; + } + }), + + /** + * Server-side start is above. Client-side start adds a notification box + */ + start: custom(function (newChromeWindow, newTarget, noreload = false) { + target = newTarget; + chromeWindow = newChromeWindow; + + return this._start(noreload); + }, { + impl: "_start" + }), + + /** + * Server-side start is above. Client-side start adds a notification box + */ + toggle: custom(function (newChromeWindow, newTarget) { + target = newTarget; + chromeWindow = newChromeWindow; + + return this._toggle(); + }, { + impl: "_toggle" + }), + + /** + * We count STARTING and STOPPING as 'running' + */ + isRunning: function () { + return isRunning; + } +}); + +exports.CSSUsageFront = CSSUsageFront; + +const knownFronts = new WeakMap(); + +/** + * Create a CSSUsageFront only when needed (returns a promise) + * For notes on target.makeRemote(), see + * https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7 + */ +exports.getUsage = function (trgt) { + return trgt.makeRemote().then(() => { + let front = knownFronts.get(trgt.client); + if (front == null && trgt.form.cssUsageActor != null) { + front = new CSSUsageFront(trgt.client, trgt.form); + knownFronts.set(trgt.client, front); + } + return front; + }); +}; diff --git a/devtools/shared/fronts/device.js b/devtools/shared/fronts/device.js new file mode 100644 index 0000000000..28f7a096a6 --- /dev/null +++ b/devtools/shared/fronts/device.js @@ -0,0 +1,54 @@ +/* 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, Cu} = require("chrome"); +const {deviceSpec} = require("devtools/shared/specs/device"); +const protocol = require("devtools/shared/protocol"); +const defer = require("devtools/shared/defer"); + +const DeviceFront = protocol.FrontClassWithSpec(deviceSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = form.deviceActor; + this.manage(this); + }, + + screenshotToBlob: function () { + return this.screenshotToDataURL().then(longstr => { + return longstr.string().then(dataURL => { + let deferred = defer(); + longstr.release().then(null, Cu.reportError); + let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"] + .createInstance(Ci.nsIXMLHttpRequest); + req.open("GET", dataURL, true); + req.responseType = "blob"; + req.onload = () => { + deferred.resolve(req.response); + }; + req.onerror = () => { + deferred.reject(req.status); + }; + req.send(); + return deferred.promise; + }); + }); + }, +}); + +const _knownDeviceFronts = new WeakMap(); + +exports.getDeviceFront = function (client, form) { + if (!form.deviceActor) { + return null; + } + + if (_knownDeviceFronts.has(client)) { + return _knownDeviceFronts.get(client); + } + + let front = new DeviceFront(client, form); + _knownDeviceFronts.set(client, front); + return front; +}; diff --git a/devtools/shared/fronts/director-manager.js b/devtools/shared/fronts/director-manager.js new file mode 100644 index 0000000000..afef42395f --- /dev/null +++ b/devtools/shared/fronts/director-manager.js @@ -0,0 +1,47 @@ +/* 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 { + messagePortSpec, + directorScriptSpec, + directorManagerSpec, +} = require("devtools/shared/specs/director-manager"); +const protocol = require("devtools/shared/protocol"); + +/** + * The corresponding Front object for the MessagePortActor. + */ +const MessagePortFront = protocol.FrontClassWithSpec(messagePortSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + +exports.MessagePortFront = MessagePortFront; + +/** + * The corresponding Front object for the DirectorScriptActor. + */ +const DirectorScriptFront = protocol.FrontClassWithSpec(directorScriptSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + +exports.DirectorScriptFront = DirectorScriptFront; + +/** + * The corresponding Front object for the DirectorManagerActor. + */ +const DirectorManagerFront = protocol.FrontClassWithSpec(directorManagerSpec, { + initialize: function (client, { directorManagerActor }) { + protocol.Front.prototype.initialize.call(this, client, { + actor: directorManagerActor + }); + this.manage(this); + } +}); + +exports.DirectorManagerFront = DirectorManagerFront; diff --git a/devtools/shared/fronts/director-registry.js b/devtools/shared/fronts/director-registry.js new file mode 100644 index 0000000000..559fe20520 --- /dev/null +++ b/devtools/shared/fronts/director-registry.js @@ -0,0 +1,21 @@ +/* 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 {directorRegistrySpec} = require("devtools/shared/specs/director-registry"); +const protocol = require("devtools/shared/protocol"); + +/** + * The corresponding Front object for the DirectorRegistryActor. + */ +const DirectorRegistryFront = protocol.FrontClassWithSpec(directorRegistrySpec, { + initialize: function (client, { directorRegistryActor }) { + protocol.Front.prototype.initialize.call(this, client, { + actor: directorRegistryActor + }); + this.manage(this); + } +}); + +exports.DirectorRegistryFront = DirectorRegistryFront; diff --git a/devtools/shared/fronts/emulation.js b/devtools/shared/fronts/emulation.js new file mode 100644 index 0000000000..99dbf565bc --- /dev/null +++ b/devtools/shared/fronts/emulation.js @@ -0,0 +1,24 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { emulationSpec } = require("devtools/shared/specs/emulation"); + +/** + * The corresponding Front object for the EmulationActor. + */ +const EmulationFront = FrontClassWithSpec(emulationSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client); + this.actorID = form.emulationActor; + this.manage(this); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, +}); + +exports.EmulationFront = EmulationFront; diff --git a/devtools/shared/fronts/eventlooplag.js b/devtools/shared/fronts/eventlooplag.js new file mode 100644 index 0000000000..7c130e621b --- /dev/null +++ b/devtools/shared/fronts/eventlooplag.js @@ -0,0 +1,15 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { eventLoopLagSpec } = require("devtools/shared/specs/eventlooplag"); + +exports.EventLoopLagFront = FrontClassWithSpec(eventLoopLagSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client); + this.actorID = form.eventLoopLagActor; + this.manage(this); + }, +}); diff --git a/devtools/shared/fronts/framerate.js b/devtools/shared/fronts/framerate.js new file mode 100644 index 0000000000..2aa678a0d2 --- /dev/null +++ b/devtools/shared/fronts/framerate.js @@ -0,0 +1,19 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { framerateSpec } = require("devtools/shared/specs/framerate"); + +/** + * The corresponding Front object for the FramerateActor. + */ +var FramerateFront = exports.FramerateFront = FrontClassWithSpec(framerateSpec, { + initialize: function (client, { framerateActor }) { + Front.prototype.initialize.call(this, client, { actor: framerateActor }); + this.manage(this); + } +}); + +exports.FramerateFront = FramerateFront; diff --git a/devtools/shared/fronts/gcli.js b/devtools/shared/fronts/gcli.js new file mode 100644 index 0000000000..28ae1138b1 --- /dev/null +++ b/devtools/shared/fronts/gcli.js @@ -0,0 +1,40 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { gcliSpec } = require("devtools/shared/specs/gcli"); + +/** + * + */ +const GcliFront = exports.GcliFront = FrontClassWithSpec(gcliSpec, { + initialize: function (client, tabForm) { + Front.prototype.initialize.call(this, client); + this.actorID = tabForm.gcliActor; + + // XXX: This is the first actor type in its hierarchy to use the protocol + // library, so we're going to self-own on the client side for now. + this.manage(this); + }, +}); + +// A cache of created fronts: WeakMap<Client, Front> +const knownFronts = new WeakMap(); + +/** + * Create a GcliFront only when needed (returns a promise) + * For notes on target.makeRemote(), see + * https://bugzilla.mozilla.org/show_bug.cgi?id=1016330#c7 + */ +exports.GcliFront.create = function (target) { + return target.makeRemote().then(() => { + let front = knownFronts.get(target.client); + if (front == null && target.form.gcliActor != null) { + front = new GcliFront(target.client, target.form); + knownFronts.set(target.client, front); + } + return front; + }); +}; diff --git a/devtools/shared/fronts/highlighters.js b/devtools/shared/fronts/highlighters.js new file mode 100644 index 0000000000..ca39ed5269 --- /dev/null +++ b/devtools/shared/fronts/highlighters.js @@ -0,0 +1,34 @@ +/* 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 { FrontClassWithSpec, custom } = require("devtools/shared/protocol"); +const { + customHighlighterSpec, + highlighterSpec +} = require("devtools/shared/specs/highlighters"); + +const HighlighterFront = FrontClassWithSpec(highlighterSpec, { + // Update the object given a form representation off the wire. + form: function (json) { + this.actorID = json.actor; + // FF42+ HighlighterActors starts exposing custom form, with traits object + this.traits = json.traits || {}; + }, + + pick: custom(function (doFocus) { + if (doFocus && this.pickAndFocus) { + return this.pickAndFocus(); + } + return this._pick(); + }, { + impl: "_pick" + }) +}); + +exports.HighlighterFront = HighlighterFront; + +const CustomHighlighterFront = FrontClassWithSpec(customHighlighterSpec, {}); + +exports.CustomHighlighterFront = CustomHighlighterFront; diff --git a/devtools/shared/fronts/inspector.js b/devtools/shared/fronts/inspector.js new file mode 100644 index 0000000000..c76b41fe7d --- /dev/null +++ b/devtools/shared/fronts/inspector.js @@ -0,0 +1,1007 @@ +/* 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"; + +require("devtools/shared/fronts/styles"); +require("devtools/shared/fronts/highlighters"); +require("devtools/shared/fronts/layout"); +const { SimpleStringFront } = require("devtools/shared/fronts/string"); +const { + Front, + FrontClassWithSpec, + custom, + preEvent, + types +} = require("devtools/shared/protocol.js"); +const { + inspectorSpec, + nodeSpec, + nodeListSpec, + walkerSpec +} = require("devtools/shared/specs/inspector"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const { Task } = require("devtools/shared/task"); +const { Class } = require("sdk/core/heritage"); +const events = require("sdk/event/core"); +const object = require("sdk/util/object"); +const nodeConstants = require("devtools/shared/dom-node-constants.js"); +loader.lazyRequireGetter(this, "CommandUtils", + "devtools/client/shared/developer-toolbar", true); + +const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__"; + +/** + * Convenience API for building a list of attribute modifications + * for the `modifyAttributes` request. + */ +const AttributeModificationList = Class({ + initialize: function (node) { + this.node = node; + this.modifications = []; + }, + + apply: function () { + let ret = this.node.modifyAttributes(this.modifications); + return ret; + }, + + destroy: function () { + this.node = null; + this.modification = null; + }, + + setAttributeNS: function (ns, name, value) { + this.modifications.push({ + attributeNamespace: ns, + attributeName: name, + newValue: value + }); + }, + + setAttribute: function (name, value) { + this.setAttributeNS(undefined, name, value); + }, + + removeAttributeNS: function (ns, name) { + this.setAttributeNS(ns, name, undefined); + }, + + removeAttribute: function (name) { + this.setAttributeNS(undefined, name, undefined); + } +}); + +/** + * Client side of the node actor. + * + * Node fronts are strored in a tree that mirrors the DOM tree on the + * server, but with a few key differences: + * - Not all children will be necessary loaded for each node. + * - The order of children isn't guaranteed to be the same as the DOM. + * Children are stored in a doubly-linked list, to make addition/removal + * and traversal quick. + * + * Due to the order/incompleteness of the child list, it is safe to use + * the parent node from clients, but the `children` request should be used + * to traverse children. + */ +const NodeFront = FrontClassWithSpec(nodeSpec, { + initialize: function (conn, form, detail, ctx) { + // The parent node + this._parent = null; + // The first child of this node. + this._child = null; + // The next sibling of this node. + this._next = null; + // The previous sibling of this node. + this._prev = null; + Front.prototype.initialize.call(this, conn, form, detail, ctx); + }, + + /** + * Destroy a node front. The node must have been removed from the + * ownership tree before this is called, unless the whole walker front + * is being destroyed. + */ + destroy: function () { + Front.prototype.destroy.call(this); + }, + + // Update the object given a form representation off the wire. + form: function (form, detail, ctx) { + if (detail === "actorid") { + this.actorID = form; + return; + } + + // backward-compatibility: shortValue indicates we are connected to old server + if (form.shortValue) { + // If the value is not complete, set nodeValue to null, it will be fetched + // when calling getNodeValue() + form.nodeValue = form.incompleteValue ? null : form.shortValue; + } + + // Shallow copy of the form. We could just store a reference, but + // eventually we'll want to update some of the data. + this._form = object.merge(form); + this._form.attrs = this._form.attrs ? this._form.attrs.slice() : []; + + if (form.parent) { + // Get the owner actor for this actor (the walker), and find the + // parent node of this actor from it, creating a standin node if + // necessary. + let parentNodeFront = ctx.marshallPool().ensureParentFront(form.parent); + this.reparent(parentNodeFront); + } + + if (form.inlineTextChild) { + this.inlineTextChild = + types.getType("domnode").read(form.inlineTextChild, ctx); + } else { + this.inlineTextChild = undefined; + } + }, + + /** + * Returns the parent NodeFront for this NodeFront. + */ + parentNode: function () { + return this._parent; + }, + + /** + * Process a mutation entry as returned from the walker's `getMutations` + * request. Only tries to handle changes of the node's contents + * themselves (character data and attribute changes), the walker itself + * will keep the ownership tree up to date. + */ + updateMutation: function (change) { + if (change.type === "attributes") { + // We'll need to lazily reparse the attributes after this change. + this._attrMap = undefined; + + // Update any already-existing attributes. + let found = false; + for (let i = 0; i < this.attributes.length; i++) { + let attr = this.attributes[i]; + if (attr.name == change.attributeName && + attr.namespace == change.attributeNamespace) { + if (change.newValue !== null) { + attr.value = change.newValue; + } else { + this.attributes.splice(i, 1); + } + found = true; + break; + } + } + // This is a new attribute. The null check is because of Bug 1192270, + // in the case of a newly added then removed attribute + if (!found && change.newValue !== null) { + this.attributes.push({ + name: change.attributeName, + namespace: change.attributeNamespace, + value: change.newValue + }); + } + } else if (change.type === "characterData") { + this._form.nodeValue = change.newValue; + } else if (change.type === "pseudoClassLock") { + this._form.pseudoClassLocks = change.pseudoClassLocks; + } else if (change.type === "events") { + this._form.hasEventListeners = change.hasEventListeners; + } + }, + + // Some accessors to make NodeFront feel more like an nsIDOMNode + + get id() { + return this.getAttribute("id"); + }, + + get nodeType() { + return this._form.nodeType; + }, + get namespaceURI() { + return this._form.namespaceURI; + }, + get nodeName() { + return this._form.nodeName; + }, + get displayName() { + let {displayName, nodeName} = this._form; + + // Keep `nodeName.toLowerCase()` for backward compatibility + return displayName || nodeName.toLowerCase(); + }, + get doctypeString() { + return "<!DOCTYPE " + this._form.name + + (this._form.publicId ? " PUBLIC \"" + this._form.publicId + "\"" : "") + + (this._form.systemId ? " \"" + this._form.systemId + "\"" : "") + + ">"; + }, + + get baseURI() { + return this._form.baseURI; + }, + + get className() { + return this.getAttribute("class") || ""; + }, + + get hasChildren() { + return this._form.numChildren > 0; + }, + get numChildren() { + return this._form.numChildren; + }, + get hasEventListeners() { + return this._form.hasEventListeners; + }, + + get isBeforePseudoElement() { + return this._form.isBeforePseudoElement; + }, + get isAfterPseudoElement() { + return this._form.isAfterPseudoElement; + }, + get isPseudoElement() { + return this.isBeforePseudoElement || this.isAfterPseudoElement; + }, + get isAnonymous() { + return this._form.isAnonymous; + }, + get isInHTMLDocument() { + return this._form.isInHTMLDocument; + }, + get tagName() { + return this.nodeType === nodeConstants.ELEMENT_NODE ? this.nodeName : null; + }, + + get isDocumentElement() { + return !!this._form.isDocumentElement; + }, + + // doctype properties + get name() { + return this._form.name; + }, + get publicId() { + return this._form.publicId; + }, + get systemId() { + return this._form.systemId; + }, + + getAttribute: function (name) { + let attr = this._getAttribute(name); + return attr ? attr.value : null; + }, + hasAttribute: function (name) { + this._cacheAttributes(); + return (name in this._attrMap); + }, + + get hidden() { + let cls = this.getAttribute("class"); + return cls && cls.indexOf(HIDDEN_CLASS) > -1; + }, + + get attributes() { + return this._form.attrs; + }, + + get pseudoClassLocks() { + return this._form.pseudoClassLocks || []; + }, + hasPseudoClassLock: function (pseudo) { + return this.pseudoClassLocks.some(locked => locked === pseudo); + }, + + get isDisplayed() { + // The NodeActor's form contains the isDisplayed information as a boolean + // starting from FF32. Before that, the property is missing + return "isDisplayed" in this._form ? this._form.isDisplayed : true; + }, + + get isTreeDisplayed() { + let parent = this; + while (parent) { + if (!parent.isDisplayed) { + return false; + } + parent = parent.parentNode(); + } + return true; + }, + + getNodeValue: custom(function () { + // backward-compatibility: if nodevalue is null and shortValue is defined, the actual + // value of the node needs to be fetched on the server. + if (this._form.nodeValue === null && this._form.shortValue) { + return this._getNodeValue(); + } + + let str = this._form.nodeValue || ""; + return promise.resolve(new SimpleStringFront(str)); + }, { + impl: "_getNodeValue" + }), + + // Accessors for custom form properties. + + getFormProperty: function (name) { + return this._form.props ? this._form.props[name] : null; + }, + + hasFormProperty: function (name) { + return this._form.props ? (name in this._form.props) : null; + }, + + get formProperties() { + return this._form.props; + }, + + /** + * Return a new AttributeModificationList for this node. + */ + startModifyingAttributes: function () { + return AttributeModificationList(this); + }, + + _cacheAttributes: function () { + if (typeof this._attrMap != "undefined") { + return; + } + this._attrMap = {}; + for (let attr of this.attributes) { + this._attrMap[attr.name] = attr; + } + }, + + _getAttribute: function (name) { + this._cacheAttributes(); + return this._attrMap[name] || undefined; + }, + + /** + * Set this node's parent. Note that the children saved in + * this tree are unordered and incomplete, so shouldn't be used + * instead of a `children` request. + */ + reparent: function (parent) { + if (this._parent === parent) { + return; + } + + if (this._parent && this._parent._child === this) { + this._parent._child = this._next; + } + if (this._prev) { + this._prev._next = this._next; + } + if (this._next) { + this._next._prev = this._prev; + } + this._next = null; + this._prev = null; + this._parent = parent; + if (!parent) { + // Subtree is disconnected, we're done + return; + } + this._next = parent._child; + if (this._next) { + this._next._prev = this; + } + parent._child = this; + }, + + /** + * Return all the known children of this node. + */ + treeChildren: function () { + let ret = []; + for (let child = this._child; child != null; child = child._next) { + ret.push(child); + } + return ret; + }, + + /** + * Do we use a local target? + * Useful to know if a rawNode is available or not. + * + * This will, one day, be removed. External code should + * not need to know if the target is remote or not. + */ + isLocalToBeDeprecated: function () { + return !!this.conn._transport._serverConnection; + }, + + /** + * Get an nsIDOMNode for the given node front. This only works locally, + * and is only intended as a stopgap during the transition to the remote + * protocol. If you depend on this you're likely to break soon. + */ + rawNode: function (rawNode) { + if (!this.isLocalToBeDeprecated()) { + console.warn("Tried to use rawNode on a remote connection."); + return null; + } + const { DebuggerServer } = require("devtools/server/main"); + let actor = DebuggerServer._searchAllConnectionsForActor(this.actorID); + if (!actor) { + // Can happen if we try to get the raw node for an already-expired + // actor. + return null; + } + return actor.rawNode; + } +}); + +exports.NodeFront = NodeFront; + +/** + * Client side of a node list as returned by querySelectorAll() + */ +const NodeListFront = FrontClassWithSpec(nodeListSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client, form); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + marshallPool: function () { + return this.parent(); + }, + + // Update the object given a form representation off the wire. + form: function (json) { + this.length = json.length; + }, + + item: custom(function (index) { + return this._item(index).then(response => { + return response.node; + }); + }, { + impl: "_item" + }), + + items: custom(function (start, end) { + return this._items(start, end).then(response => { + return response.nodes; + }); + }, { + impl: "_items" + }) +}); + +exports.NodeListFront = NodeListFront; + +/** + * Client side of the DOM walker. + */ +const WalkerFront = FrontClassWithSpec(walkerSpec, { + // Set to true if cleanup should be requested after every mutation list. + autoCleanup: true, + + /** + * This is kept for backward-compatibility reasons with older remote target. + * Targets previous to bug 916443 + */ + pick: custom(function () { + return this._pick().then(response => { + return response.node; + }); + }, {impl: "_pick"}), + + initialize: function (client, form) { + this._createRootNodePromise(); + Front.prototype.initialize.call(this, client, form); + this._orphaned = new Set(); + this._retainedOrphans = new Set(); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + // Update the object given a form representation off the wire. + form: function (json) { + this.actorID = json.actor; + this.rootNode = types.getType("domnode").read(json.root, this); + this._rootNodeDeferred.resolve(this.rootNode); + // FF42+ the actor starts exposing traits + this.traits = json.traits || {}; + }, + + /** + * Clients can use walker.rootNode to get the current root node of the + * walker, but during a reload the root node might be null. This + * method returns a promise that will resolve to the root node when it is + * set. + */ + getRootNode: function () { + return this._rootNodeDeferred.promise; + }, + + /** + * Create the root node promise, triggering the "new-root" notification + * on resolution. + */ + _createRootNodePromise: function () { + this._rootNodeDeferred = defer(); + this._rootNodeDeferred.promise.then(() => { + events.emit(this, "new-root"); + }); + }, + + /** + * When reading an actor form off the wire, we want to hook it up to its + * parent front. The protocol guarantees that the parent will be seen + * by the client in either a previous or the current request. + * So if we've already seen this parent return it, otherwise create + * a bare-bones stand-in node. The stand-in node will be updated + * with a real form by the end of the deserialization. + */ + ensureParentFront: function (id) { + let front = this.get(id); + if (front) { + return front; + } + + return types.getType("domnode").read({ actor: id }, this, "standin"); + }, + + /** + * See the documentation for WalkerActor.prototype.retainNode for + * information on retained nodes. + * + * From the client's perspective, `retainNode` can fail if the node in + * question is removed from the ownership tree before the `retainNode` + * request reaches the server. This can only happen if the client has + * asked the server to release nodes but hasn't gotten a response + * yet: Either a `releaseNode` request or a `getMutations` with `cleanup` + * set is outstanding. + * + * If either of those requests is outstanding AND releases the retained + * node, this request will fail with noSuchActor, but the ownership tree + * will stay in a consistent state. + * + * Because the protocol guarantees that requests will be processed and + * responses received in the order they were sent, we get the right + * semantics by setting our local retained flag on the node only AFTER + * a SUCCESSFUL retainNode call. + */ + retainNode: custom(function (node) { + return this._retainNode(node).then(() => { + node.retained = true; + }); + }, { + impl: "_retainNode", + }), + + unretainNode: custom(function (node) { + return this._unretainNode(node).then(() => { + node.retained = false; + if (this._retainedOrphans.has(node)) { + this._retainedOrphans.delete(node); + this._releaseFront(node); + } + }); + }, { + impl: "_unretainNode" + }), + + releaseNode: custom(function (node, options = {}) { + // NodeFront.destroy will destroy children in the ownership tree too, + // mimicking what the server will do here. + let actorID = node.actorID; + this._releaseFront(node, !!options.force); + return this._releaseNode({ actorID: actorID }); + }, { + impl: "_releaseNode" + }), + + findInspectingNode: custom(function () { + return this._findInspectingNode().then(response => { + return response.node; + }); + }, { + impl: "_findInspectingNode" + }), + + querySelector: custom(function (queryNode, selector) { + return this._querySelector(queryNode, selector).then(response => { + return response.node; + }); + }, { + impl: "_querySelector" + }), + + getNodeActorFromObjectActor: custom(function (objectActorID) { + return this._getNodeActorFromObjectActor(objectActorID).then(response => { + return response ? response.node : null; + }); + }, { + impl: "_getNodeActorFromObjectActor" + }), + + getStyleSheetOwnerNode: custom(function (styleSheetActorID) { + return this._getStyleSheetOwnerNode(styleSheetActorID).then(response => { + return response ? response.node : null; + }); + }, { + impl: "_getStyleSheetOwnerNode" + }), + + getNodeFromActor: custom(function (actorID, path) { + return this._getNodeFromActor(actorID, path).then(response => { + return response ? response.node : null; + }); + }, { + impl: "_getNodeFromActor" + }), + + /* + * Incrementally search the document for a given string. + * For modern servers, results will be searched with using the WalkerActor + * `search` function (includes tag names, attributes, and text contents). + * Only 1 result is sent back, and calling the method again with the same + * query will send the next result. When there are no more results to be sent + * back, null is sent. + * @param {String} query + * @param {Object} options + * - "reverse": search backwards + * - "selectorOnly": treat input as a selector string (don't search text + * tags, attributes, etc) + */ + search: custom(Task.async(function* (query, options = { }) { + let nodeList; + let searchType; + let searchData = this.searchData = this.searchData || { }; + let selectorOnly = !!options.selectorOnly; + + // Backwards compat. Use selector only search if the new + // search functionality isn't implemented, or if the caller (tests) + // want it. + if (selectorOnly || !this.traits.textSearch) { + searchType = "selector"; + if (this.traits.multiFrameQuerySelectorAll) { + nodeList = yield this.multiFrameQuerySelectorAll(query); + } else { + nodeList = yield this.querySelectorAll(this.rootNode, query); + } + } else { + searchType = "search"; + let result = yield this._search(query, options); + nodeList = result.list; + } + + // If this is a new search, start at the beginning. + if (searchData.query !== query || + searchData.selectorOnly !== selectorOnly) { + searchData.selectorOnly = selectorOnly; + searchData.query = query; + searchData.index = -1; + } + + if (!nodeList.length) { + return null; + } + + // Move search result cursor and cycle if necessary. + searchData.index = options.reverse ? searchData.index - 1 : + searchData.index + 1; + if (searchData.index >= nodeList.length) { + searchData.index = 0; + } + if (searchData.index < 0) { + searchData.index = nodeList.length - 1; + } + + // Send back the single node, along with any relevant search data + let node = yield nodeList.item(searchData.index); + return { + type: searchType, + node: node, + resultsLength: nodeList.length, + resultsIndex: searchData.index, + }; + }), { + impl: "_search" + }), + + _releaseFront: function (node, force) { + if (node.retained && !force) { + node.reparent(null); + this._retainedOrphans.add(node); + return; + } + + if (node.retained) { + // Forcing a removal. + this._retainedOrphans.delete(node); + } + + // Release any children + for (let child of node.treeChildren()) { + this._releaseFront(child, force); + } + + // All children will have been removed from the node by this point. + node.reparent(null); + node.destroy(); + }, + + /** + * Get any unprocessed mutation records and process them. + */ + getMutations: custom(function (options = {}) { + return this._getMutations(options).then(mutations => { + let emitMutations = []; + for (let change of mutations) { + // The target is only an actorID, get the associated front. + let targetID; + let targetFront; + + if (change.type === "newRoot") { + // We may receive a new root without receiving any documentUnload + // beforehand. Like when opening tools in middle of a document load. + if (this.rootNode) { + this._createRootNodePromise(); + } + this.rootNode = types.getType("domnode").read(change.target, this); + this._rootNodeDeferred.resolve(this.rootNode); + targetID = this.rootNode.actorID; + targetFront = this.rootNode; + } else { + targetID = change.target; + targetFront = this.get(targetID); + } + + if (!targetFront) { + console.trace("Got a mutation for an unexpected actor: " + targetID + + ", please file a bug on bugzilla.mozilla.org!"); + continue; + } + + let emittedMutation = object.merge(change, { target: targetFront }); + + if (change.type === "childList" || + change.type === "nativeAnonymousChildList") { + // Update the ownership tree according to the mutation record. + let addedFronts = []; + let removedFronts = []; + for (let removed of change.removed) { + let removedFront = this.get(removed); + if (!removedFront) { + console.error("Got a removal of an actor we didn't know about: " + + removed); + continue; + } + // Remove from the ownership tree + removedFront.reparent(null); + + // This node is orphaned unless we get it in the 'added' list + // eventually. + this._orphaned.add(removedFront); + removedFronts.push(removedFront); + } + for (let added of change.added) { + let addedFront = this.get(added); + if (!addedFront) { + console.error("Got an addition of an actor we didn't know " + + "about: " + added); + continue; + } + addedFront.reparent(targetFront); + + // The actor is reconnected to the ownership tree, unorphan + // it. + this._orphaned.delete(addedFront); + addedFronts.push(addedFront); + } + + // Before passing to users, replace the added and removed actor + // ids with front in the mutation record. + emittedMutation.added = addedFronts; + emittedMutation.removed = removedFronts; + + // If this is coming from a DOM mutation, the actor's numChildren + // was passed in. Otherwise, it is simulated from a frame load or + // unload, so don't change the front's form. + if ("numChildren" in change) { + targetFront._form.numChildren = change.numChildren; + } + } else if (change.type === "frameLoad") { + // Nothing we need to do here, except verify that we don't have any + // document children, because we should have gotten a documentUnload + // first. + for (let child of targetFront.treeChildren()) { + if (child.nodeType === nodeConstants.DOCUMENT_NODE) { + console.trace("Got an unexpected frameLoad in the inspector, " + + "please file a bug on bugzilla.mozilla.org!"); + } + } + } else if (change.type === "documentUnload") { + if (targetFront === this.rootNode) { + this._createRootNodePromise(); + } + + // We try to give fronts instead of actorIDs, but these fronts need + // to be destroyed now. + emittedMutation.target = targetFront.actorID; + emittedMutation.targetParent = targetFront.parentNode(); + + // Release the document node and all of its children, even retained. + this._releaseFront(targetFront, true); + } else if (change.type === "unretained") { + // Retained orphans were force-released without the intervention of + // client (probably a navigated frame). + for (let released of change.nodes) { + let releasedFront = this.get(released); + this._retainedOrphans.delete(released); + this._releaseFront(releasedFront, true); + } + } else { + targetFront.updateMutation(change); + } + + // Update the inlineTextChild property of the target for a selected list of + // mutation types. + if (change.type === "inlineTextChild" || + change.type === "childList" || + change.type === "nativeAnonymousChildList") { + if (change.inlineTextChild) { + targetFront.inlineTextChild = + types.getType("domnode").read(change.inlineTextChild, this); + } else { + targetFront.inlineTextChild = undefined; + } + } + + emitMutations.push(emittedMutation); + } + + if (options.cleanup) { + for (let node of this._orphaned) { + // This will move retained nodes to this._retainedOrphans. + this._releaseFront(node); + } + this._orphaned = new Set(); + } + + events.emit(this, "mutations", emitMutations); + }); + }, { + impl: "_getMutations" + }), + + /** + * Handle the `new-mutations` notification by fetching the + * available mutation records. + */ + onMutations: preEvent("new-mutations", function () { + // Fetch and process the mutations. + this.getMutations({cleanup: this.autoCleanup}).catch(() => {}); + }), + + isLocal: function () { + return !!this.conn._transport._serverConnection; + }, + + // XXX hack during transition to remote inspector: get a proper NodeFront + // for a given local node. Only works locally. + frontForRawNode: function (rawNode) { + if (!this.isLocal()) { + console.warn("Tried to use frontForRawNode on a remote connection."); + return null; + } + const { DebuggerServer } = require("devtools/server/main"); + let walkerActor = DebuggerServer._searchAllConnectionsForActor(this.actorID); + if (!walkerActor) { + throw Error("Could not find client side for actor " + this.actorID); + } + let nodeActor = walkerActor._ref(rawNode); + + // Pass the node through a read/write pair to create the client side actor. + let nodeType = types.getType("domnode"); + let returnNode = nodeType.read( + nodeType.write(nodeActor, walkerActor), this); + let top = returnNode; + let extras = walkerActor.parents(nodeActor, {sameTypeRootTreeItem: true}); + for (let extraActor of extras) { + top = nodeType.read(nodeType.write(extraActor, walkerActor), this); + } + + if (top !== this.rootNode) { + // Imported an already-orphaned node. + this._orphaned.add(top); + walkerActor._orphaned + .add(DebuggerServer._searchAllConnectionsForActor(top.actorID)); + } + return returnNode; + }, + + removeNode: custom(Task.async(function* (node) { + let previousSibling = yield this.previousSibling(node); + let nextSibling = yield this._removeNode(node); + return { + previousSibling: previousSibling, + nextSibling: nextSibling, + }; + }), { + impl: "_removeNode" + }), +}); + +exports.WalkerFront = WalkerFront; + +/** + * Client side of the inspector actor, which is used to create + * inspector-related actors, including the walker. + */ +var InspectorFront = FrontClassWithSpec(inspectorSpec, { + initialize: function (client, tabForm) { + Front.prototype.initialize.call(this, client); + this.actorID = tabForm.inspectorActor; + + // XXX: This is the first actor type in its hierarchy to use the protocol + // library, so we're going to self-own on the client side for now. + this.manage(this); + }, + + destroy: function () { + delete this.walker; + Front.prototype.destroy.call(this); + }, + + getWalker: custom(function (options = {}) { + return this._getWalker(options).then(walker => { + this.walker = walker; + return walker; + }); + }, { + impl: "_getWalker" + }), + + getPageStyle: custom(function () { + return this._getPageStyle().then(pageStyle => { + // We need a walker to understand node references from the + // node style. + if (this.walker) { + return pageStyle; + } + return this.getWalker().then(() => { + return pageStyle; + }); + }); + }, { + impl: "_getPageStyle" + }), + + pickColorFromPage: custom(Task.async(function* (toolbox, options) { + if (toolbox) { + // If the eyedropper was already started using the gcli command, hide it so we don't + // end up with 2 instances of the eyedropper on the page. + let {target} = toolbox; + let requisition = yield CommandUtils.createRequisition(target, { + environment: CommandUtils.createEnvironment({target}) + }); + yield requisition.updateExec("eyedropper --hide"); + } + + yield this._pickColorFromPage(options); + }), { + impl: "_pickColorFromPage" + }) +}); + +exports.InspectorFront = InspectorFront; diff --git a/devtools/shared/fronts/layout.js b/devtools/shared/fronts/layout.js new file mode 100644 index 0000000000..5a1a6185d3 --- /dev/null +++ b/devtools/shared/fronts/layout.js @@ -0,0 +1,30 @@ +/* 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 { FrontClassWithSpec } = require("devtools/shared/protocol"); +const { gridSpec, layoutSpec } = require("devtools/shared/specs/layout"); + +const GridFront = FrontClassWithSpec(gridSpec, { + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this._form = form; + }, + + /** + * Getter for the grid fragments data. + */ + get gridFragments() { + return this._form.gridFragments; + } +}); + +const LayoutFront = FrontClassWithSpec(layoutSpec, {}); + +exports.GridFront = GridFront; +exports.LayoutFront = LayoutFront; diff --git a/devtools/shared/fronts/memory.js b/devtools/shared/fronts/memory.js new file mode 100644 index 0000000000..d7a6a3108e --- /dev/null +++ b/devtools/shared/fronts/memory.js @@ -0,0 +1,92 @@ +/* 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 { memorySpec } = require("devtools/shared/specs/memory"); +const { Task } = require("devtools/shared/task"); +const protocol = require("devtools/shared/protocol"); + +loader.lazyRequireGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm", true); +loader.lazyRequireGetter(this, "HeapSnapshotFileUtils", + "devtools/shared/heapsnapshot/HeapSnapshotFileUtils"); + +const MemoryFront = protocol.FrontClassWithSpec(memorySpec, { + initialize: function (client, form, rootForm = null) { + protocol.Front.prototype.initialize.call(this, client, form); + this._client = client; + this.actorID = form.memoryActor; + this.heapSnapshotFileActorID = rootForm + ? rootForm.heapSnapshotFileActor + : null; + this.manage(this); + }, + + /** + * Save a heap snapshot, transfer it from the server to the client if the + * server and client do not share a file system, and return the local file + * path to the heap snapshot. + * + * Note that this is safe to call for actors inside sandoxed child processes, + * as we jump through the correct IPDL hoops. + * + * @params Boolean options.forceCopy + * Always force a bulk data copy of the saved heap snapshot, even when + * the server and client share a file system. + * + * @params {Object|undefined} options.boundaries + * The boundaries for the heap snapshot. See + * ThreadSafeChromeUtils.webidl for more details. + * + * @returns Promise<String> + */ + saveHeapSnapshot: protocol.custom(Task.async(function* (options = {}) { + const snapshotId = yield this._saveHeapSnapshotImpl(options.boundaries); + + if (!options.forceCopy && + (yield HeapSnapshotFileUtils.haveHeapSnapshotTempFile(snapshotId))) { + return HeapSnapshotFileUtils.getHeapSnapshotTempFilePath(snapshotId); + } + + return yield this.transferHeapSnapshot(snapshotId); + }), { + impl: "_saveHeapSnapshotImpl" + }), + + /** + * Given that we have taken a heap snapshot with the given id, transfer the + * heap snapshot file to the client. The path to the client's local file is + * returned. + * + * @param {String} snapshotId + * + * @returns Promise<String> + */ + transferHeapSnapshot: protocol.custom(function (snapshotId) { + if (!this.heapSnapshotFileActorID) { + throw new Error("MemoryFront initialized without a rootForm"); + } + + const request = this._client.request({ + to: this.heapSnapshotFileActorID, + type: "transferHeapSnapshot", + snapshotId + }); + + return new Promise((resolve, reject) => { + const outFilePath = + HeapSnapshotFileUtils.getNewUniqueHeapSnapshotTempFilePath(); + const outFile = new FileUtils.File(outFilePath); + + const outFileStream = FileUtils.openSafeFileOutputStream(outFile); + request.on("bulk-reply", Task.async(function* ({ copyTo }) { + yield copyTo(outFileStream); + FileUtils.closeSafeFileOutputStream(outFileStream); + resolve(outFilePath); + })); + }); + }) +}); + +exports.MemoryFront = MemoryFront; diff --git a/devtools/shared/fronts/moz.build b/devtools/shared/fronts/moz.build new file mode 100644 index 0000000000..8a38d6b5d3 --- /dev/null +++ b/devtools/shared/fronts/moz.build @@ -0,0 +1,41 @@ +# -*- 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.js', + 'addons.js', + 'animation.js', + 'call-watcher.js', + 'canvas.js', + 'css-properties.js', + 'csscoverage.js', + 'device.js', + 'director-manager.js', + 'director-registry.js', + 'emulation.js', + 'eventlooplag.js', + 'framerate.js', + 'gcli.js', + 'highlighters.js', + 'inspector.js', + 'layout.js', + 'memory.js', + 'performance-entries.js', + 'performance-recording.js', + 'performance.js', + 'preference.js', + 'profiler.js', + 'promises.js', + 'reflow.js', + 'settings.js', + 'storage.js', + 'string.js', + 'styles.js', + 'stylesheets.js', + 'timeline.js', + 'webaudio.js', + 'webgl.js' +) diff --git a/devtools/shared/fronts/performance-entries.js b/devtools/shared/fronts/performance-entries.js new file mode 100644 index 0000000000..5f3236530d --- /dev/null +++ b/devtools/shared/fronts/performance-entries.js @@ -0,0 +1,17 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const performanceSpec = require("devtools/shared/specs/performance-entries"); + +var PerformanceEntriesFront = FrontClassWithSpec(performanceSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client); + this.actorID = form.performanceEntriesActor; + this.manage(this); + }, +}); + +exports.PerformanceEntriesFront = PerformanceEntriesFront; diff --git a/devtools/shared/fronts/performance-recording.js b/devtools/shared/fronts/performance-recording.js new file mode 100644 index 0000000000..09c61d4ee5 --- /dev/null +++ b/devtools/shared/fronts/performance-recording.js @@ -0,0 +1,152 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { performanceRecordingSpec } = require("devtools/shared/specs/performance-recording"); + +loader.lazyRequireGetter(this, "PerformanceIO", + "devtools/client/performance/modules/io"); +loader.lazyRequireGetter(this, "PerformanceRecordingCommon", + "devtools/shared/performance/recording-common", true); +loader.lazyRequireGetter(this, "RecordingUtils", + "devtools/shared/performance/recording-utils"); +loader.lazyRequireGetter(this, "merge", "sdk/util/object", true); + +/** + * This can be used on older Profiler implementations, but the methods cannot + * be changed -- you must introduce a new method, and detect the server. + */ +const PerformanceRecordingFront = FrontClassWithSpec(performanceRecordingSpec, merge({ + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this.actorID = form.actor; + this._form = form; + this._configuration = form.configuration; + this._startingBufferStatus = form.startingBufferStatus; + this._console = form.console; + this._label = form.label; + this._startTime = form.startTime; + this._localStartTime = form.localStartTime; + this._recording = form.recording; + this._completed = form.completed; + this._duration = form.duration; + + if (form.finalizedData) { + this._profile = form.profile; + this._systemHost = form.systemHost; + this._systemClient = form.systemClient; + } + + // Sort again on the client side if we're using realtime markers and the recording + // just finished. This is because GC/Compositing markers can come into the array out + // of order with the other markers, leading to strange collapsing in waterfall view. + if (this._completed && !this._markersSorted) { + this._markers = this._markers.sort((a, b) => (a.start > b.start)); + this._markersSorted = true; + } + }, + + initialize: function (client, form, config) { + Front.prototype.initialize.call(this, client, form); + this._markers = []; + this._frames = []; + this._memory = []; + this._ticks = []; + this._allocations = { sites: [], timestamps: [], frames: [], sizes: [] }; + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + /** + * Saves the current recording to a file. + * + * @param nsILocalFile file + * The file to stream the data into. + */ + exportRecording: function (file) { + let recordingData = this.getAllData(); + return PerformanceIO.saveRecordingToFile(recordingData, file); + }, + + /** + * Fired whenever the PerformanceFront emits markers, memory or ticks. + */ + _addTimelineData: function (eventName, data) { + let config = this.getConfiguration(); + + switch (eventName) { + // Accumulate timeline markers into an array. Furthermore, the timestamps + // do not have a zero epoch, so offset all of them by the start time. + case "markers": { + if (!config.withMarkers) { + break; + } + let { markers } = data; + RecordingUtils.offsetMarkerTimes(markers, this._startTime); + RecordingUtils.pushAll(this._markers, markers); + break; + } + // Accumulate stack frames into an array. + case "frames": { + if (!config.withMarkers) { + break; + } + let { frames } = data; + RecordingUtils.pushAll(this._frames, frames); + break; + } + // Accumulate memory measurements into an array. Furthermore, the timestamp + // does not have a zero epoch, so offset it by the actor's start time. + case "memory": { + if (!config.withMemory) { + break; + } + let { delta, measurement } = data; + this._memory.push({ + delta: delta - this._startTime, + value: measurement.total / 1024 / 1024 + }); + break; + } + // Save the accumulated refresh driver ticks. + case "ticks": { + if (!config.withTicks) { + break; + } + let { timestamps } = data; + this._ticks = timestamps; + break; + } + // Accumulate allocation sites into an array. + case "allocations": { + if (!config.withAllocations) { + break; + } + let { + allocations: sites, + allocationsTimestamps: timestamps, + allocationSizes: sizes, + frames, + } = data; + + RecordingUtils.offsetAndScaleTimestamps(timestamps, this._startTime); + RecordingUtils.pushAll(this._allocations.sites, sites); + RecordingUtils.pushAll(this._allocations.timestamps, timestamps); + RecordingUtils.pushAll(this._allocations.frames, frames); + RecordingUtils.pushAll(this._allocations.sizes, sizes); + break; + } + } + }, + + toString: () => "[object PerformanceRecordingFront]" +}, PerformanceRecordingCommon)); + +exports.PerformanceRecordingFront = PerformanceRecordingFront; diff --git a/devtools/shared/fronts/performance.js b/devtools/shared/fronts/performance.js new file mode 100644 index 0000000000..da5a9ffb07 --- /dev/null +++ b/devtools/shared/fronts/performance.js @@ -0,0 +1,148 @@ +/* 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"); +const { Front, FrontClassWithSpec, custom, preEvent } = require("devtools/shared/protocol"); +const { PerformanceRecordingFront } = require("devtools/shared/fronts/performance-recording"); +const { performanceSpec } = require("devtools/shared/specs/performance"); +const { Task } = require("devtools/shared/task"); + +loader.lazyRequireGetter(this, "PerformanceIO", + "devtools/client/performance/modules/io"); +loader.lazyRequireGetter(this, "LegacyPerformanceFront", + "devtools/client/performance/legacy/front", true); +loader.lazyRequireGetter(this, "getSystemInfo", + "devtools/shared/system", true); + +const PerformanceFront = FrontClassWithSpec(performanceSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client, form); + this.actorID = form.performanceActor; + this.manage(this); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + /** + * Conenct to the server, and handle once-off tasks like storing traits + * or system info. + */ + connect: custom(Task.async(function* () { + let systemClient = yield getSystemInfo(); + let { traits } = yield this._connect({ systemClient }); + this._traits = traits; + + return this._traits; + }), { + impl: "_connect" + }), + + get traits() { + if (!this._traits) { + Cu.reportError("Cannot access traits of PerformanceFront before " + + "calling `connect()`."); + } + return this._traits; + }, + + /** + * Pass in a PerformanceRecording and get a normalized value from 0 to 1 of how much + * of this recording's lifetime remains without being overwritten. + * + * @param {PerformanceRecording} recording + * @return {number?} + */ + getBufferUsageForRecording: function (recording) { + if (!recording.isRecording()) { + return void 0; + } + let { + position: currentPosition, + totalSize, + generation: currentGeneration + } = this._currentBufferStatus; + let { + position: origPosition, + generation: origGeneration + } = recording.getStartingBufferStatus(); + + let normalizedCurrent = (totalSize * (currentGeneration - origGeneration)) + + currentPosition; + let percent = (normalizedCurrent - origPosition) / totalSize; + + // Clamp between 0 and 1; can get negative percentage values when a new + // recording starts and the currentBufferStatus has not yet been updated. Rather + // than fetching another status update, just clamp to 0, and this will be updated + // on the next profiler-status event. + if (percent < 0) { + return 0; + } else if (percent > 1) { + return 1; + } + + return percent; + }, + + /** + * Loads a recording from a file. + * + * @param {nsILocalFile} file + * The file to import the data from. + * @return {Promise<PerformanceRecordingFront>} + */ + importRecording: function (file) { + return PerformanceIO.loadRecordingFromFile(file).then(recordingData => { + let model = new PerformanceRecordingFront(); + model._imported = true; + model._label = recordingData.label || ""; + model._duration = recordingData.duration; + model._markers = recordingData.markers; + model._frames = recordingData.frames; + model._memory = recordingData.memory; + model._ticks = recordingData.ticks; + model._allocations = recordingData.allocations; + model._profile = recordingData.profile; + model._configuration = recordingData.configuration || {}; + model._systemHost = recordingData.systemHost; + model._systemClient = recordingData.systemClient; + return model; + }); + }, + + /** + * Store profiler status when the position has been update so we can + * calculate recording's buffer percentage usage after emitting the event. + */ + _onProfilerStatus: preEvent("profiler-status", function (data) { + this._currentBufferStatus = data; + }), + + /** + * For all PerformanceRecordings that are recording, and needing realtime markers, + * apply the timeline data to the front PerformanceRecording (so we only have one event + * for each timeline data chunk as they could be shared amongst several recordings). + */ + _onTimelineEvent: preEvent("timeline-data", function (type, data, recordings) { + for (let recording of recordings) { + recording._addTimelineData(type, data); + } + }), +}); + +exports.PerformanceFront = PerformanceFront; + +exports.createPerformanceFront = function createPerformanceFront(target) { + // If we force legacy mode, or the server does not have a performance actor (< Fx42), + // use our LegacyPerformanceFront which will handle + // the communication over RDP to other underlying actors. + if (target.TEST_PERFORMANCE_LEGACY_FRONT || !target.form.performanceActor) { + return new LegacyPerformanceFront(target); + } + // If our server has a PerformanceActor implementation, set this + // up like a normal front. + return new PerformanceFront(target.client, target.form); +}; diff --git a/devtools/shared/fronts/preference.js b/devtools/shared/fronts/preference.js new file mode 100644 index 0000000000..22b3f912f9 --- /dev/null +++ b/devtools/shared/fronts/preference.js @@ -0,0 +1,31 @@ +/* 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 {preferenceSpec} = require("devtools/shared/specs/preference"); +const protocol = require("devtools/shared/protocol"); + +const PreferenceFront = protocol.FrontClassWithSpec(preferenceSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = form.preferenceActor; + this.manage(this); + }, +}); + +const _knownPreferenceFronts = new WeakMap(); + +exports.getPreferenceFront = function (client, form) { + if (!form.preferenceActor) { + return null; + } + + if (_knownPreferenceFronts.has(client)) { + return _knownPreferenceFronts.get(client); + } + + let front = new PreferenceFront(client, form); + _knownPreferenceFronts.set(client, front); + return front; +}; diff --git a/devtools/shared/fronts/profiler.js b/devtools/shared/fronts/profiler.js new file mode 100644 index 0000000000..f564513e3f --- /dev/null +++ b/devtools/shared/fronts/profiler.js @@ -0,0 +1,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 { Cu } = require("chrome"); +const { + Front, + FrontClassWithSpec, + custom +} = require("devtools/shared/protocol"); +const { profilerSpec } = require("devtools/shared/specs/profiler"); + +loader.lazyRequireGetter(this, "events", "sdk/event/core"); +loader.lazyRequireGetter(this, "extend", "sdk/util/object", true); + +/** + * This can be used on older Profiler implementations, but the methods cannot + * be changed -- you must introduce a new method, and detect the server. + */ +exports.ProfilerFront = FrontClassWithSpec(profilerSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client, form); + this.actorID = form.profilerActor; + this.manage(this); + + this._onProfilerEvent = this._onProfilerEvent.bind(this); + events.on(this, "*", this._onProfilerEvent); + }, + + destroy: function () { + events.off(this, "*", this._onProfilerEvent); + Front.prototype.destroy.call(this); + }, + + /** + * If using the protocol.js Fronts, then make stringify default, + * since the read/write mechanisms will expose it as an object anyway, but + * this lets other consumers who connect directly (xpcshell tests, Gecko Profiler) to + * have unchanged behaviour. + */ + getProfile: custom(function (options) { + return this._getProfile(extend({ stringify: true }, options)); + }, { + impl: "_getProfile" + }), + + /** + * Also emit an old `eventNotification` for older consumers of the profiler. + */ + _onProfilerEvent: function (eventName, data) { + // If this event already passed through once, don't repropagate + if (data.relayed) { + return; + } + data.relayed = true; + + if (eventName === "eventNotification") { + // If this is `eventNotification`, this is coming from an older Gecko (<Fx42) + // that doesn't use protocol.js style events. Massage it to emit a protocol.js + // style event as well. + events.emit(this, data.topic, data); + } else { + // Otherwise if a modern protocol.js event, emit it also as `eventNotification` + // for compatibility reasons on the client (like for any add-ons/Gecko Profiler + // using this event) and log a deprecation message if there is a listener. + this.conn.emit("eventNotification", { + subject: data.subject, + topic: data.topic, + data: data.data, + details: data.details + }); + if (this.conn._getListeners("eventNotification").length) { + Cu.reportError(` + ProfilerActor's "eventNotification" on the DebuggerClient has been deprecated. + Use the ProfilerFront found in "devtools/server/actors/profiler".`); + } + } + }, +}); diff --git a/devtools/shared/fronts/promises.js b/devtools/shared/fronts/promises.js new file mode 100644 index 0000000000..72896455d7 --- /dev/null +++ b/devtools/shared/fronts/promises.js @@ -0,0 +1,27 @@ +/* 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 { + Front, + FrontClassWithSpec, +} = require("devtools/shared/protocol"); +const { promisesSpec } = require("devtools/shared/specs/promises"); + +/** + * PromisesFront, the front for the PromisesActor. + */ +const PromisesFront = FrontClassWithSpec(promisesSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client, form); + this.actorID = form.promisesActor; + this.manage(this); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + } +}); + +exports.PromisesFront = PromisesFront; diff --git a/devtools/shared/fronts/reflow.js b/devtools/shared/fronts/reflow.js new file mode 100644 index 0000000000..69c5132700 --- /dev/null +++ b/devtools/shared/fronts/reflow.js @@ -0,0 +1,29 @@ +/* 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 {reflowSpec} = require("devtools/shared/specs/reflow"); +const protocol = require("devtools/shared/protocol"); + +/** + * Usage example of the reflow front: + * + * let front = ReflowFront(toolbox.target.client, toolbox.target.form); + * front.on("reflows", this._onReflows); + * front.start(); + * // now wait for events to come + */ +const ReflowFront = protocol.FrontClassWithSpec(reflowSpec, { + initialize: function (client, {reflowActor}) { + protocol.Front.prototype.initialize.call(this, client, {actor: reflowActor}); + this.manage(this); + }, + + destroy: function () { + protocol.Front.prototype.destroy.call(this); + }, +}); + +exports.ReflowFront = ReflowFront; diff --git a/devtools/shared/fronts/settings.js b/devtools/shared/fronts/settings.js new file mode 100644 index 0000000000..158425364e --- /dev/null +++ b/devtools/shared/fronts/settings.js @@ -0,0 +1,29 @@ +/* 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 {settingsSpec} = require("devtools/shared/specs/settings"); +const protocol = require("devtools/shared/protocol"); + +const SettingsFront = protocol.FrontClassWithSpec(settingsSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = form.settingsActor; + this.manage(this); + }, +}); + +const _knownSettingsFronts = new WeakMap(); + +exports.getSettingsFront = function (client, form) { + if (!form.settingsActor) { + return null; + } + if (_knownSettingsFronts.has(client)) { + return _knownSettingsFronts.get(client); + } + let front = new SettingsFront(client, form); + _knownSettingsFronts.set(client, front); + return front; +}; diff --git a/devtools/shared/fronts/storage.js b/devtools/shared/fronts/storage.js new file mode 100644 index 0000000000..304e57c6fe --- /dev/null +++ b/devtools/shared/fronts/storage.js @@ -0,0 +1,32 @@ +/* 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 protocol = require("devtools/shared/protocol"); +const specs = require("devtools/shared/specs/storage"); + +for (let childSpec of Object.values(specs.childSpecs)) { + protocol.FrontClassWithSpec(childSpec, { + form(form, detail) { + if (detail === "actorid") { + this.actorID = form; + return null; + } + + this.actorID = form.actor; + this.hosts = form.hosts; + return null; + } + }); +} + +const StorageFront = protocol.FrontClassWithSpec(specs.storageSpec, { + initialize(client, tabForm) { + protocol.Front.prototype.initialize.call(this, client); + this.actorID = tabForm.storageActor; + this.manage(this); + } +}); + +exports.StorageFront = StorageFront; diff --git a/devtools/shared/fronts/string.js b/devtools/shared/fronts/string.js new file mode 100644 index 0000000000..12036afe43 --- /dev/null +++ b/devtools/shared/fronts/string.js @@ -0,0 +1,47 @@ +/* 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 {DebuggerServer} = require("devtools/server/main"); +const promise = require("promise"); +const {longStringSpec, SimpleStringFront} = require("devtools/shared/specs/string"); +const protocol = require("devtools/shared/protocol"); + +const LongStringFront = protocol.FrontClassWithSpec(longStringSpec, { + initialize: function (client) { + protocol.Front.prototype.initialize.call(this, client); + }, + + destroy: function () { + this.initial = null; + this.length = null; + this.strPromise = null; + protocol.Front.prototype.destroy.call(this); + }, + + form: function (form) { + this.actorID = form.actor; + this.initial = form.initial; + this.length = form.length; + }, + + string: function () { + if (!this.strPromise) { + let promiseRest = (thusFar) => { + if (thusFar.length === this.length) { + return promise.resolve(thusFar); + } + return this.substring(thusFar.length, + thusFar.length + DebuggerServer.LONG_STRING_READ_LENGTH) + .then((next) => promiseRest(thusFar + next)); + }; + + this.strPromise = promiseRest(this.initial); + } + return this.strPromise; + } +}); + +exports.LongStringFront = LongStringFront; +exports.SimpleStringFront = SimpleStringFront; diff --git a/devtools/shared/fronts/styleeditor.js b/devtools/shared/fronts/styleeditor.js new file mode 100644 index 0000000000..5feb6df1b6 --- /dev/null +++ b/devtools/shared/fronts/styleeditor.js @@ -0,0 +1,113 @@ +/* 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 { SimpleStringFront } = require("devtools/shared/fronts/string"); +const { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { + oldStyleSheetSpec, + styleEditorSpec +} = require("devtools/shared/specs/styleeditor"); +const promise = require("promise"); +const defer = require("devtools/shared/defer"); +const events = require("sdk/event/core"); + +/** + * StyleSheetFront is the client-side counterpart to a StyleSheetActor. + */ +const OldStyleSheetFront = FrontClassWithSpec(oldStyleSheetSpec, { + initialize: function (conn, form, ctx, detail) { + Front.prototype.initialize.call(this, conn, form, ctx, detail); + + this._onPropertyChange = this._onPropertyChange.bind(this); + events.on(this, "property-change", this._onPropertyChange); + }, + + destroy: function () { + events.off(this, "property-change", this._onPropertyChange); + + Front.prototype.destroy.call(this); + }, + + _onPropertyChange: function (property, value) { + this._form[property] = value; + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this.actorID = form.actor; + this._form = form; + }, + + getText: function () { + let deferred = defer(); + + events.once(this, "source-load", (source) => { + let longStr = new SimpleStringFront(source); + deferred.resolve(longStr); + }); + this.fetchSource(); + + return deferred.promise; + }, + + getOriginalSources: function () { + return promise.resolve([]); + }, + + get href() { + return this._form.href; + }, + get nodeHref() { + return this._form.nodeHref; + }, + get disabled() { + return !!this._form.disabled; + }, + get title() { + return this._form.title; + }, + get isSystem() { + return this._form.system; + }, + get styleSheetIndex() { + return this._form.styleSheetIndex; + }, + get ruleCount() { + return this._form.ruleCount; + } +}); + +exports.OldStyleSheetFront = OldStyleSheetFront; + +/** + * The corresponding Front object for the StyleEditorActor. + */ +const StyleEditorFront = FrontClassWithSpec(styleEditorSpec, { + initialize: function (client, tabForm) { + Front.prototype.initialize.call(this, client); + this.actorID = tabForm.styleEditorActor; + this.manage(this); + }, + + getStyleSheets: function () { + let deferred = defer(); + + events.once(this, "document-load", (styleSheets) => { + deferred.resolve(styleSheets); + }); + this.newDocument(); + + return deferred.promise; + }, + + addStyleSheet: function (text) { + return this.newStyleSheet(text); + } +}); + +exports.StyleEditorFront = StyleEditorFront; diff --git a/devtools/shared/fronts/styles.js b/devtools/shared/fronts/styles.js new file mode 100644 index 0000000000..116bb1f75a --- /dev/null +++ b/devtools/shared/fronts/styles.js @@ -0,0 +1,421 @@ +/* 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"; + +require("devtools/shared/fronts/stylesheets"); +const { + Front, + FrontClassWithSpec, + custom, + preEvent +} = require("devtools/shared/protocol"); +const { + pageStyleSpec, + styleRuleSpec +} = require("devtools/shared/specs/styles"); +const promise = require("promise"); +const { Task } = require("devtools/shared/task"); +const { Class } = require("sdk/core/heritage"); +const { RuleRewriter } = require("devtools/shared/css/parsing-utils"); + +/** + * PageStyleFront, the front object for the PageStyleActor + */ +const PageStyleFront = FrontClassWithSpec(pageStyleSpec, { + initialize: function (conn, form, ctx, detail) { + Front.prototype.initialize.call(this, conn, form, ctx, detail); + this.inspector = this.parent(); + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this._form = form; + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + get walker() { + return this.inspector.walker; + }, + + get supportsAuthoredStyles() { + return this._form.traits && this._form.traits.authoredStyles; + }, + + getMatchedSelectors: custom(function (node, property, options) { + return this._getMatchedSelectors(node, property, options).then(ret => { + return ret.matched; + }); + }, { + impl: "_getMatchedSelectors" + }), + + getApplied: custom(Task.async(function* (node, options = {}) { + // If the getApplied method doesn't recreate the style cache itself, this + // means a call to cssLogic.highlight is required before trying to access + // the applied rules. Issue a request to getLayout if this is the case. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1103993#c16. + if (!this._form.traits || !this._form.traits.getAppliedCreatesStyleCache) { + yield this.getLayout(node); + } + let ret = yield this._getApplied(node, options); + return ret.entries; + }), { + impl: "_getApplied" + }), + + addNewRule: custom(function (node, pseudoClasses) { + let addPromise; + if (this.supportsAuthoredStyles) { + addPromise = this._addNewRule(node, pseudoClasses, true); + } else { + addPromise = this._addNewRule(node, pseudoClasses); + } + return addPromise.then(ret => { + return ret.entries[0]; + }); + }, { + impl: "_addNewRule" + }) +}); + +exports.PageStyleFront = PageStyleFront; + +/** + * StyleRuleFront, the front for the StyleRule actor. + */ +const StyleRuleFront = FrontClassWithSpec(styleRuleSpec, { + initialize: function (client, form, ctx, detail) { + Front.prototype.initialize.call(this, client, form, ctx, detail); + }, + + destroy: function () { + Front.prototype.destroy.call(this); + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this.actorID = form.actor; + this._form = form; + if (this._mediaText) { + this._mediaText = null; + } + }, + + /** + * Ensure _form is updated when location-changed is emitted. + */ + _locationChangedPre: preEvent("location-changed", function (line, column) { + this._clearOriginalLocation(); + this._form.line = line; + this._form.column = column; + }), + + /** + * Return a new RuleModificationList or RuleRewriter for this node. + * A RuleRewriter will be returned when the rule's canSetRuleText + * trait is true; otherwise a RuleModificationList will be + * returned. + * + * @param {CssPropertiesFront} cssProperties + * This is needed by the RuleRewriter. + * @return {RuleModificationList} + */ + startModifyingProperties: function (cssProperties) { + if (this.canSetRuleText) { + return new RuleRewriter(cssProperties.isKnown, this, this.authoredText); + } + return new RuleModificationList(this); + }, + + get type() { + return this._form.type; + }, + get line() { + return this._form.line || -1; + }, + get column() { + return this._form.column || -1; + }, + get cssText() { + return this._form.cssText; + }, + get authoredText() { + return this._form.authoredText || this._form.cssText; + }, + get declarations() { + return this._form.declarations || []; + }, + get keyText() { + return this._form.keyText; + }, + get name() { + return this._form.name; + }, + get selectors() { + return this._form.selectors; + }, + get media() { + return this._form.media; + }, + get mediaText() { + if (!this._form.media) { + return null; + } + if (this._mediaText) { + return this._mediaText; + } + this._mediaText = this.media.join(", "); + return this._mediaText; + }, + + get parentRule() { + return this.conn.getActor(this._form.parentRule); + }, + + get parentStyleSheet() { + return this.conn.getActor(this._form.parentStyleSheet); + }, + + get element() { + return this.conn.getActor(this._form.element); + }, + + get href() { + if (this._form.href) { + return this._form.href; + } + let sheet = this.parentStyleSheet; + return sheet ? sheet.href : ""; + }, + + get nodeHref() { + let sheet = this.parentStyleSheet; + return sheet ? sheet.nodeHref : ""; + }, + + get supportsModifySelectorUnmatched() { + return this._form.traits && this._form.traits.modifySelectorUnmatched; + }, + + get canSetRuleText() { + return this._form.traits && this._form.traits.canSetRuleText; + }, + + get location() { + return { + source: this.parentStyleSheet, + href: this.href, + line: this.line, + column: this.column + }; + }, + + _clearOriginalLocation: function () { + this._originalLocation = null; + }, + + getOriginalLocation: function () { + if (this._originalLocation) { + return promise.resolve(this._originalLocation); + } + let parentSheet = this.parentStyleSheet; + if (!parentSheet) { + // This rule doesn't belong to a stylesheet so it is an inline style. + // Inline styles do not have any mediaText so we can return early. + return promise.resolve(this.location); + } + return parentSheet.getOriginalLocation(this.line, this.column) + .then(({ fromSourceMap, source, line, column }) => { + let location = { + href: source, + line: line, + column: column, + mediaText: this.mediaText + }; + if (fromSourceMap === false) { + location.source = this.parentStyleSheet; + } + if (!source) { + location.href = this.href; + } + this._originalLocation = location; + return location; + }); + }, + + modifySelector: custom(Task.async(function* (node, value) { + let response; + if (this.supportsModifySelectorUnmatched) { + // If the debugee supports adding unmatched rules (post FF41) + if (this.canSetRuleText) { + response = yield this.modifySelector2(node, value, true); + } else { + response = yield this.modifySelector2(node, value); + } + } else { + response = yield this._modifySelector(value); + } + + if (response.ruleProps) { + response.ruleProps = response.ruleProps.entries[0]; + } + return response; + }), { + impl: "_modifySelector" + }), + + setRuleText: custom(function (newText) { + this._form.authoredText = newText; + return this._setRuleText(newText); + }, { + impl: "_setRuleText" + }) +}); + +exports.StyleRuleFront = StyleRuleFront; + +/** + * Convenience API for building a list of attribute modifications + * for the `modifyProperties` request. A RuleModificationList holds a + * list of modifications that will be applied to a StyleRuleActor. + * The modifications are processed in the order in which they are + * added to the RuleModificationList. + * + * Objects of this type expose the same API as @see RuleRewriter. + * This lets the inspector use (mostly) the same code, regardless of + * whether the server implements setRuleText. + */ +var RuleModificationList = Class({ + /** + * Initialize a RuleModificationList. + * @param {StyleRuleFront} rule the associated rule + */ + initialize: function (rule) { + this.rule = rule; + this.modifications = []; + }, + + /** + * Apply the modifications in this object to the associated rule. + * + * @return {Promise} A promise which will be resolved when the modifications + * are complete; @see StyleRuleActor.modifyProperties. + */ + apply: function () { + return this.rule.modifyProperties(this.modifications); + }, + + /** + * Add a "set" entry to the modification list. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name the property's name + * @param {String} value the property's value + * @param {String} priority the property's priority, either the empty + * string or "important" + */ + setProperty: function (index, name, value, priority) { + this.modifications.push({ + type: "set", + name: name, + value: value, + priority: priority + }); + }, + + /** + * Add a "remove" entry to the modification list. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name the name of the property to remove + */ + removeProperty: function (index, name) { + this.modifications.push({ + type: "remove", + name: name + }); + }, + + /** + * Rename a property. This implementation acts like + * |removeProperty|, because |setRuleText| is not available. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name current name of the property + * + * This parameter is also passed, but as it is not used in this + * implementation, it is omitted. It is documented here as this + * code also defined the interface implemented by @see RuleRewriter. + * @param {String} newName new name of the property + */ + renameProperty: function (index, name) { + this.removeProperty(index, name); + }, + + /** + * Enable or disable a property. This implementation acts like + * |removeProperty| when disabling, or a no-op when enabling, + * because |setRuleText| is not available. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name current name of the property + * @param {Boolean} isEnabled true if the property should be enabled; + * false if it should be disabled + */ + setPropertyEnabled: function (index, name, isEnabled) { + if (!isEnabled) { + this.removeProperty(index, name); + } + }, + + /** + * Create a new property. This implementation does nothing, because + * |setRuleText| is not available. + * + * These parameters are passed, but as they are not used in this + * implementation, they are omitted. They are documented here as + * this code also defined the interface implemented by @see + * RuleRewriter. + * + * @param {Number} index index of the property in the rule. + * This can be -1 in the case where + * the rule does not support setRuleText; + * generally for setting properties + * on an element's style. + * @param {String} name name of the new property + * @param {String} value value of the new property + * @param {String} priority priority of the new property; either + * the empty string or "important" + * @param {Boolean} enabled True if the new property should be + * enabled, false if disabled + */ + createProperty: function () { + // Nothing. + }, +}); diff --git a/devtools/shared/fronts/stylesheets.js b/devtools/shared/fronts/stylesheets.js new file mode 100644 index 0000000000..6df8a9bd46 --- /dev/null +++ b/devtools/shared/fronts/stylesheets.js @@ -0,0 +1,184 @@ +/* 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 { Front, FrontClassWithSpec } = require("devtools/shared/protocol"); +const { + getIndentationFromPrefs, + getIndentationFromString +} = require("devtools/shared/indentation"); +const { + originalSourceSpec, + mediaRuleSpec, + styleSheetSpec, + styleSheetsSpec +} = require("devtools/shared/specs/stylesheets"); +const promise = require("promise"); +const { Task } = require("devtools/shared/task"); +const events = require("sdk/event/core"); + +/** + * The client-side counterpart for an OriginalSourceActor. + */ +const OriginalSourceFront = FrontClassWithSpec(originalSourceSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client, form); + + this.isOriginalSource = true; + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this.actorID = form.actor; + this._form = form; + }, + + get href() { + return this._form.url; + }, + get url() { + return this._form.url; + } +}); + +exports.OriginalSourceFront = OriginalSourceFront; + +/** + * Corresponding client-side front for a MediaRuleActor. + */ +const MediaRuleFront = FrontClassWithSpec(mediaRuleSpec, { + initialize: function (client, form) { + Front.prototype.initialize.call(this, client, form); + + this._onMatchesChange = this._onMatchesChange.bind(this); + events.on(this, "matches-change", this._onMatchesChange); + }, + + _onMatchesChange: function (matches) { + this._form.matches = matches; + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this.actorID = form.actor; + this._form = form; + }, + + get mediaText() { + return this._form.mediaText; + }, + get conditionText() { + return this._form.conditionText; + }, + get matches() { + return this._form.matches; + }, + get line() { + return this._form.line || -1; + }, + get column() { + return this._form.column || -1; + }, + get parentStyleSheet() { + return this.conn.getActor(this._form.parentStyleSheet); + } +}); + +exports.MediaRuleFront = MediaRuleFront; + +/** + * StyleSheetFront is the client-side counterpart to a StyleSheetActor. + */ +const StyleSheetFront = FrontClassWithSpec(styleSheetSpec, { + initialize: function (conn, form) { + Front.prototype.initialize.call(this, conn, form); + + this._onPropertyChange = this._onPropertyChange.bind(this); + events.on(this, "property-change", this._onPropertyChange); + }, + + destroy: function () { + events.off(this, "property-change", this._onPropertyChange); + Front.prototype.destroy.call(this); + }, + + _onPropertyChange: function (property, value) { + this._form[property] = value; + }, + + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + this.actorID = form.actor; + this._form = form; + }, + + get href() { + return this._form.href; + }, + get nodeHref() { + return this._form.nodeHref; + }, + get disabled() { + return !!this._form.disabled; + }, + get title() { + return this._form.title; + }, + get isSystem() { + return this._form.system; + }, + get styleSheetIndex() { + return this._form.styleSheetIndex; + }, + get ruleCount() { + return this._form.ruleCount; + }, + + /** + * Get the indentation to use for edits to this style sheet. + * + * @return {Promise} A promise that will resolve to a string that + * should be used to indent a block in this style sheet. + */ + guessIndentation: function () { + let prefIndent = getIndentationFromPrefs(); + if (prefIndent) { + let {indentUnit, indentWithTabs} = prefIndent; + return promise.resolve(indentWithTabs ? "\t" : " ".repeat(indentUnit)); + } + + return Task.spawn(function* () { + let longStr = yield this.getText(); + let source = yield longStr.string(); + + let {indentUnit, indentWithTabs} = getIndentationFromString(source); + + return indentWithTabs ? "\t" : " ".repeat(indentUnit); + }.bind(this)); + } +}); + +exports.StyleSheetFront = StyleSheetFront; + +/** + * The corresponding Front object for the StyleSheetsActor. + */ +const StyleSheetsFront = FrontClassWithSpec(styleSheetsSpec, { + initialize: function (client, tabForm) { + Front.prototype.initialize.call(this, client); + this.actorID = tabForm.styleSheetsActor; + this.manage(this); + } +}); + +exports.StyleSheetsFront = StyleSheetsFront; diff --git a/devtools/shared/fronts/timeline.js b/devtools/shared/fronts/timeline.js new file mode 100644 index 0000000000..cd6bc54e87 --- /dev/null +++ b/devtools/shared/fronts/timeline.js @@ -0,0 +1,25 @@ +/* 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 { + Front, + FrontClassWithSpec, +} = require("devtools/shared/protocol"); +const { timelineSpec } = require("devtools/shared/specs/timeline"); + +/** + * TimelineFront, the front for the TimelineActor. + */ +const TimelineFront = FrontClassWithSpec(timelineSpec, { + initialize: function (client, { timelineActor }) { + Front.prototype.initialize.call(this, client, { actor: timelineActor }); + this.manage(this); + }, + destroy: function () { + Front.prototype.destroy.call(this); + }, +}); + +exports.TimelineFront = TimelineFront; diff --git a/devtools/shared/fronts/webaudio.js b/devtools/shared/fronts/webaudio.js new file mode 100644 index 0000000000..73594c5130 --- /dev/null +++ b/devtools/shared/fronts/webaudio.js @@ -0,0 +1,83 @@ +/* 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 { + audionodeSpec, + webAudioSpec, + AUTOMATION_METHODS, + NODE_CREATION_METHODS, + NODE_ROUTING_METHODS, +} = require("devtools/shared/specs/webaudio"); +const protocol = require("devtools/shared/protocol"); +const AUDIO_NODE_DEFINITION = require("devtools/server/actors/utils/audionodes.json"); + +/** + * The corresponding Front object for the AudioNodeActor. + * + * @attribute {String} type + * The type of audio node, like "OscillatorNode", "MediaElementAudioSourceNode" + * @attribute {Boolean} source + * Boolean indicating if the node is a source node, like BufferSourceNode, + * MediaElementAudioSourceNode, OscillatorNode, etc. + * @attribute {Boolean} bypassable + * Boolean indicating if the audio node is bypassable (splitter, + * merger and destination nodes, for example, are not) + */ +const AudioNodeFront = protocol.FrontClassWithSpec(audionodeSpec, { + form: function (form, detail) { + if (detail === "actorid") { + this.actorID = form; + return; + } + + this.actorID = form.actor; + this.type = form.type; + this.source = form.source; + this.bypassable = form.bypassable; + }, + + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + // if we were manually passed a form, this was created manually and + // needs to own itself for now. + if (form) { + this.manage(this); + } + } +}); + +exports.AudioNodeFront = AudioNodeFront; + +/** + * The corresponding Front object for the WebAudioActor. + */ +const WebAudioFront = protocol.FrontClassWithSpec(webAudioSpec, { + initialize: function (client, { webaudioActor }) { + protocol.Front.prototype.initialize.call(this, client, { actor: webaudioActor }); + this.manage(this); + }, + + /** + * If connecting to older geckos (<Fx43), where audio node actor's do not + * contain `type`, `source` and `bypassable` properties, fetch + * them manually here. + */ + _onCreateNode: protocol.preEvent("create-node", function (audionode) { + if (!audionode.type) { + return audionode.getType().then(type => { + audionode.type = type; + audionode.source = !!AUDIO_NODE_DEFINITION[type].source; + audionode.bypassable = !AUDIO_NODE_DEFINITION[type].unbypassable; + }); + } + return null; + }), +}); + +WebAudioFront.AUTOMATION_METHODS = new Set(AUTOMATION_METHODS); +WebAudioFront.NODE_CREATION_METHODS = new Set(NODE_CREATION_METHODS); +WebAudioFront.NODE_ROUTING_METHODS = new Set(NODE_ROUTING_METHODS); + +exports.WebAudioFront = WebAudioFront; diff --git a/devtools/shared/fronts/webgl.js b/devtools/shared/fronts/webgl.js new file mode 100644 index 0000000000..b5f7afac8c --- /dev/null +++ b/devtools/shared/fronts/webgl.js @@ -0,0 +1,45 @@ +/* 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 { + shaderSpec, + programSpec, + webGLSpec, +} = require("devtools/shared/specs/webgl"); +const protocol = require("devtools/shared/protocol"); + +/** + * The corresponding Front object for the ShaderActor. + */ +const ShaderFront = protocol.FrontClassWithSpec(shaderSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + +exports.ShaderFront = ShaderFront; + +/** + * The corresponding Front object for the ProgramActor. + */ +const ProgramFront = protocol.FrontClassWithSpec(programSpec, { + initialize: function (client, form) { + protocol.Front.prototype.initialize.call(this, client, form); + } +}); + +exports.ProgramFront = ProgramFront; + +/** + * The corresponding Front object for the WebGLActor. + */ +const WebGLFront = protocol.FrontClassWithSpec(webGLSpec, { + initialize: function (client, { webglActor }) { + protocol.Front.prototype.initialize.call(this, client, { actor: webglActor }); + this.manage(this); + } +}); + +exports.WebGLFront = WebGLFront; |