/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const { Task } = require("resource://gre/modules/Task.jsm"); const { method, Arg, Option, RetVal, Front, FrontClass, Actor, ActorClass } = require("devtools/server/protocol"); const events = require("sdk/event/core"); const { createSystem } = require("gcli/system"); /** * Manage remote connections that want to talk to GCLI */ const GcliActor = ActorClass({ typeName: "gcli", events: { "commands-changed" : { type: "commandsChanged" } }, initialize: function(conn, tabActor) { Actor.prototype.initialize.call(this, conn); this._commandsChanged = this._commandsChanged.bind(this); this._tabActor = tabActor; this._requisitionPromise = undefined; // see _getRequisition() }, disconnect: function() { return this.destroy(); }, destroy: function() { Actor.prototype.destroy.call(this); // If _getRequisition has not been called, just bail quickly if (this._requisitionPromise == null) { this._commandsChanged = undefined; this._tabActor = undefined; return Promise.resolve(); } return this._getRequisition().then(requisition => { requisition.destroy(); this._system.commands.onCommandsChange.remove(this._commandsChanged); this._system.destroy(); this._system = undefined; this._requisitionPromise = undefined; this._tabActor = undefined; this._commandsChanged = undefined; }); }, /** * Load a module into the requisition */ _testOnly_addItemsByModule: method(function(names) { return this._getRequisition().then(requisition => { return requisition.system.addItemsByModule(names); }); }, { request: { customProps: Arg(0, "array:string") } }), /** * Unload a module from the requisition */ _testOnly_removeItemsByModule: method(function(names) { return this._getRequisition().then(requisition => { return requisition.system.removeItemsByModule(names); }); }, { request: { customProps: Arg(0, "array:string") } }), /** * Retrieve a list of the remotely executable commands * @param customProps Array of strings containing additional properties which, * if specified in the command spec, will be included in the JSON. Normally we * transfer only the properties required for GCLI to function. */ specs: method(function(customProps) { return this._getRequisition().then(requisition => { return requisition.system.commands.getCommandSpecs(customProps); }); }, { request: { customProps: Arg(0, "nullable:array:string") }, response: { value: RetVal("array:json") } }), /** * Execute a GCLI command * @return a promise of an object with the following properties: * - data: The output of the command * - type: The type of the data to allow selection of a converter * - error: True if the output was considered an error */ execute: method(function(typed) { return this._getRequisition().then(requisition => { return requisition.updateExec(typed).then(output => output.toJson()); }); }, { request: { typed: Arg(0, "string") // The command string }, response: RetVal("json") }), /** * Get the state of an input string. i.e. requisition.getStateData() */ state: method(function(typed, start, rank) { return this._getRequisition().then(requisition => { return requisition.update(typed).then(() => { return requisition.getStateData(start, rank); }); }); }, { request: { typed: Arg(0, "string"), // The command string start: Arg(1, "number"), // Cursor start position rank: Arg(2, "number") // The prediction offset (# times UP/DOWN pressed) }, response: RetVal("json") }), /** * Call type.parse to check validity. Used by the remote type * @return a promise of an object with the following properties: * - status: Of of the following strings: VALID|INCOMPLETE|ERROR * - message: The message to display to the user * - predictions: An array of suggested values for the given parameter */ parseType: method(function(typed, paramName) { return this._getRequisition().then(requisition => { return requisition.update(typed).then(() => { let assignment = requisition.getAssignment(paramName); return Promise.resolve(assignment.predictions).then(predictions => { return { status: assignment.getStatus().toString(), message: assignment.message, predictions: predictions }; }); }); }); }, { request: { typed: Arg(0, "string"), // The command string paramName: Arg(1, "string") // The name of the parameter to parse }, response: RetVal("json") }), /** * Get the incremented/decremented value of some type * @return a promise of a string containing the new argument text */ nudgeType: method(function(typed, by, paramName) { return this.requisition.update(typed).then(() => { const assignment = this.requisition.getAssignment(paramName); return this.requisition.nudge(assignment, by).then(() => { return assignment.arg == null ? undefined : assignment.arg.text; }); }); }, { request: { typed: Arg(0, "string"), // The command string by: Arg(1, "number"), // +1/-1 for increment / decrement paramName: Arg(2, "string") // The name of the parameter to parse }, response: RetVal("string") }), /** * Perform a lookup on a selection type to get the allowed values */ getSelectionLookup: method(function(commandName, paramName) { return this._getRequisition().then(requisition => { const command = requisition.system.commands.get(commandName); if (command == null) { throw new Error("No command called '" + commandName + "'"); } let type; command.params.forEach(param => { if (param.name === paramName) { type = param.type; } }); if (type == null) { throw new Error("No parameter called '" + paramName + "' in '" + commandName + "'"); } const reply = type.getLookup(requisition.executionContext); return Promise.resolve(reply).then(lookup => { // lookup returns an array of objects with name/value properties and // the values might not be JSONable, so remove them return lookup.map(info => ({ name: info.name })); }); }); }, { request: { commandName: Arg(0, "string"), // The command containing the parameter in question paramName: Arg(1, "string"), // The name of the parameter }, response: RetVal("json") }), /** * Lazy init for a Requisition */ _getRequisition: function() { if (this._tabActor == null) { throw new Error('GcliActor used post-destroy'); } if (this._requisitionPromise != null) { return this._requisitionPromise; } const Requisition = require("gcli/cli").Requisition; const tabActor = this._tabActor; this._system = createSystem({ location: "server" }); this._system.commands.onCommandsChange.add(this._commandsChanged); const gcliInit = require("devtools/shared/gcli/commands/index"); gcliInit.addAllItemsByModule(this._system); // this._requisitionPromise should be created synchronously with the call // to _getRequisition so that destroy can tell whether there is an async // init in progress this._requisitionPromise = this._system.load().then(() => { const environment = { get chromeWindow() { throw new Error("environment.chromeWindow is not available in runAt:server commands"); }, get chromeDocument() { throw new Error("environment.chromeDocument is not available in runAt:server commands"); }, get window() { return tabActor.window; }, get document() { return tabActor.window && tabActor.window.document; } }; return new Requisition(this._system, { environment: environment }); }); return this._requisitionPromise; }, /** * Pass events from requisition.system.commands.onCommandsChange upwards */ _commandsChanged: function() { events.emit(this, "commands-changed"); }, }); exports.GcliActor = GcliActor; /** * */ const GcliFront = exports.GcliFront = FrontClass(GcliActor, { 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 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; }); };