tenfourfox/devtools/server/actors/gcli.js
Cameron Kaiser c9b2922b70 hello FPR
2017-04-19 00:56:45 -07:00

320 lines
9.4 KiB
JavaScript

/* 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<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;
});
};