From d59796be4dcbccdf78d58d7e9780968580dd4c38 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 4 Feb 2015 15:02:14 +0000 Subject: [PATCH 01/42] runat-1128988: Use system rather than 'gcli' In many cases there is a gcli global that is a system object. We want to make it clearer what we're working with and to get rid of the global The changes to cli.js rid us of a useless function and make it clearer how system is used inside a Requisition. Signed-off-by: Joe Walker --- lib/gcli/cli.js | 14 ++------------ lib/gcli/commands/commands.js | 2 -- lib/gcli/commands/server/firefox.js | 4 ++-- lib/gcli/connectors/index.js | 14 +++++++------- lib/gcli/settings.js | 1 - lib/gcli/ui/menu.js | 2 +- lib/gcli/ui/terminal.js | 2 +- 7 files changed, 13 insertions(+), 26 deletions(-) diff --git a/lib/gcli/cli.js b/lib/gcli/cli.js index 687cd7d5..76c22d9d 100644 --- a/lib/gcli/cli.js +++ b/lib/gcli/cli.js @@ -100,16 +100,6 @@ var removeMapping = function(requisition) { instances.splice(index, 1); }; -/** - * Some manual intervention is needed in parsing the { command. - */ -function getEvalCommand(commands) { - if (getEvalCommand._cmd == null) { - getEvalCommand._cmd = commands.get(evalCmd.name); - } - return getEvalCommand._cmd; -} - /** * Assignment is a link between a parameter and the data for that parameter. * The data for the parameter is available as in the preferred type and as @@ -1786,8 +1776,8 @@ Requisition.prototype._split = function(args) { if (args[0].type === 'ScriptArgument') { // Special case: if the user enters { console.log('foo'); } then we need to // use the hidden 'eval' command - conversion = new Conversion(getEvalCommand(this.system.commands), - new ScriptArgument()); + var command = this.system.commands.get(evalCmd.name); + conversion = new Conversion(command, new ScriptArgument()); this._setAssignmentInternal(this.commandAssignment, conversion); return; } diff --git a/lib/gcli/commands/commands.js b/lib/gcli/commands/commands.js index be2cf775..a1f073e8 100644 --- a/lib/gcli/commands/commands.js +++ b/lib/gcli/commands/commands.js @@ -344,8 +344,6 @@ function Commands(types) { /** * Add a command to the list of known commands. - * This function is exposed to the outside world (via gcli/index). It is - * documented in docs/index.md for all the world to see. * @param commandSpec The command and its metadata. * @return The new command */ diff --git a/lib/gcli/commands/server/firefox.js b/lib/gcli/commands/server/firefox.js index b362a209..488dbb61 100644 --- a/lib/gcli/commands/server/firefox.js +++ b/lib/gcli/commands/server/firefox.js @@ -139,11 +139,11 @@ function createCommonJsToJsTestFilter() { ' return Task.spawn(function() {\n' + ' let options = yield helpers.openTab(TEST_URI);\n' + ' yield helpers.openToolbar(options);\n' + - ' gcli.addItems(mockCommands.items);\n' + + ' options.requisition.system.addItems(mockCommands.items);\n' + '\n' + ' yield helpers.runTests(options, exports);\n' + '\n' + - ' gcli.removeItems(mockCommands.items);\n' + + ' options.requisition.system.removeItems(mockCommands.items);\n' + ' yield helpers.closeToolbar(options);\n' + ' yield helpers.closeTab(options);\n' + ' }).then(finish, helpers.handleError);\n' + diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 5e353193..67915d16 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -32,7 +32,7 @@ require('../util/legacy'); * - lib/gcli/index.js: Generic basic set (without commands) * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox + * - lib/gcli/index.js: From scratch listing for Firefox * - lib/gcli/connectors/index.js: Client only items when executing remotely * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI */ @@ -132,10 +132,10 @@ exports.connect = function(options) { }); }; -exports.addItems = function(gcli, specs, connection) { - exports.removeRemoteItems(gcli, connection); +exports.addItems = function(system, specs, connection) { + exports.removeRemoteItems(system, connection); var remoteItems = exports.addLocalFunctions(specs, connection); - gcli.addItems(remoteItems); + system.addItems(remoteItems); }; /** @@ -176,10 +176,10 @@ exports.addLocalFunctions = function(specs, connection) { return specs; }; -exports.removeRemoteItems = function(gcli, connection) { - gcli.commands.getAll().forEach(function(command) { +exports.removeRemoteItems = function(system, connection) { + system.commands.getAll().forEach(function(command) { if (command.connection === connection) { - gcli.commands.remove(command); + system.commands.remove(command); } }); }; diff --git a/lib/gcli/settings.js b/lib/gcli/settings.js index 9f23f833..44be7309 100644 --- a/lib/gcli/settings.js +++ b/lib/gcli/settings.js @@ -67,7 +67,6 @@ Settings.prototype.getAll = function(filter) { /** * Add a new setting - * @return The new Setting object */ Settings.prototype.add = function(prefSpec) { var type = this._types.createType(prefSpec.type); diff --git a/lib/gcli/ui/menu.js b/lib/gcli/ui/menu.js index fef02103..6ba00e76 100644 --- a/lib/gcli/ui/menu.js +++ b/lib/gcli/ui/menu.js @@ -256,7 +256,7 @@ function getHighlightingProxy(item, match, document) { } /** - * @return The current choice index + * @return {int} current choice index */ Menu.prototype.getChoiceIndex = function() { return this._choice == null ? 0 : this._choice; diff --git a/lib/gcli/ui/terminal.js b/lib/gcli/ui/terminal.js index 2e7629d2..fb2bbaa6 100644 --- a/lib/gcli/ui/terminal.js +++ b/lib/gcli/ui/terminal.js @@ -42,7 +42,7 @@ function Terminal() { /** * A wrapper to take care of the functions concerning an input element - * @param components Object that links to other UI components. GCLI provided: + * @param options Object that links to other UI components. GCLI provided: * - requisition * - document */ From 9813ead800b33e392082d639ba5b6ef5f5176050 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 4 Feb 2015 15:08:45 +0000 Subject: [PATCH 02/42] runat-1128988: Output doesn't need to cache a conversionContext Its only use of context is in a function into which a context is passed so we can just remove it as a member variable and all is good. Signed-off-by: Joe Walker --- lib/gcli/cli.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gcli/cli.js b/lib/gcli/cli.js index 76c22d9d..e0697978 100644 --- a/lib/gcli/cli.js +++ b/lib/gcli/cli.js @@ -2035,7 +2035,7 @@ Requisition.prototype.exec = function(options) { typed = typed.replace(/\s*}\s*$/, ''); } - var output = new Output(this.conversionContext, { + var output = new Output({ command: command, args: args, typed: typed, @@ -2124,14 +2124,13 @@ exports.Requisition = Requisition; /** * A simple object to hold information about the output of a command */ -function Output(context, options) { +function Output(options) { options = options || {}; this.command = options.command || ''; this.args = options.args || {}; this.typed = options.typed || ''; this.canonical = options.canonical || ''; this.hidden = options.hidden === true ? true : false; - this.converters = context.system.converters; this.type = undefined; this.data = undefined; @@ -2176,7 +2175,8 @@ Output.prototype.complete = function(data, error) { * Call converters.convert using the data in this Output object */ Output.prototype.convert = function(type, conversionContext) { - return this.converters.convert(this.data, this.type, type, conversionContext); + var converters = conversionContext.system.converters; + return converters.convert(this.data, this.type, type, conversionContext); }; Output.prototype.toJson = function() { From 60c4415fa2cf8fe120eab1d01f9ba6cfdb92d00d Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 4 Feb 2015 15:09:54 +0000 Subject: [PATCH 03/42] runat-1128988: Remove call to setTimeout because this Promise is async Previously Promise was synchronous, so an optional 'async' made sense. Signed-off-by: Joe Walker --- lib/gcli/connectors/direct.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index 249637d3..f9dc380d 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -82,12 +82,6 @@ exports.items = [ } ]; -/** - * Direct connection is mostly for testing. Async is closer to what things like - * websocket will actually give us, but sync gives us clearer stack traces. - */ -var asyncCall = false; - function DirectConnection() { this._emit = this._emit.bind(this); this.remoter = new Remoter(requisition); @@ -98,26 +92,10 @@ DirectConnection.prototype = Object.create(Connection.prototype); DirectConnection.prototype.call = function(command, data) { return new Promise(function(resolve, reject) { - if (asyncCall) { - setTimeout(function() { - this._call(resolve, reject, command, data); - }, 1); - } - else { - this._call(resolve, reject, command, data); - } - }.bind(this)); -}; - -DirectConnection.prototype._call = function(resolve, reject, command, data) { - try { var func = this.remoter.exposed[command]; var reply = func.call(this.remoter, data); resolve(reply); - } - catch (ex) { - reject(ex); - } + }.bind(this)); }; DirectConnection.prototype.disconnect = function() { From 1c90caca963292a1151b82358b875daff316b3e0 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 4 Feb 2015 15:10:25 +0000 Subject: [PATCH 04/42] runat-1128988: Add missing bind Signed-off-by: Joe Walker --- lib/gcli/settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gcli/settings.js b/lib/gcli/settings.js index 44be7309..42231042 100644 --- a/lib/gcli/settings.js +++ b/lib/gcli/settings.js @@ -46,7 +46,7 @@ Settings.prototype.setDefaults = function(newValues) { if (this._settingValues[name] === undefined) { this._settingValues[name] = newValues[name]; } - }); + }.bind(this)); }; /** From 7bc315ff6ab5310f92ea1cc05a8b0f1255ec4710 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Wed, 4 Feb 2015 15:12:25 +0000 Subject: [PATCH 05/42] runat-1128988: Escape the { in a regexp so it's not a match count Signed-off-by: Joe Walker --- lib/gcli/cli.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gcli/cli.js b/lib/gcli/cli.js index e0697978..2e6e15b4 100644 --- a/lib/gcli/cli.js +++ b/lib/gcli/cli.js @@ -308,7 +308,7 @@ var evalCmd = { var reply = customEval(args.javascript); return context.typedData(typeof reply, reply); }, - isCommandRegexp: /^\s*{\s*/ + isCommandRegexp: /^\s*\{\s*/ }; exports.items = [ evalCmd ]; From ddc3506638b3d334744f51a762c3649b929f7b01 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:17:58 +0000 Subject: [PATCH 06/42] runat-1128988: Use a better default connection If we're asked for the default connector, we used to just use the first in the list, but it makes more sense to use the last one we asked for by name. Signed-off-by: Joe Walker --- lib/gcli/connectors/connectors.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/gcli/connectors/connectors.js b/lib/gcli/connectors/connectors.js index cf543823..edc24e86 100644 --- a/lib/gcli/connectors/connectors.js +++ b/lib/gcli/connectors/connectors.js @@ -138,14 +138,21 @@ Connectors.prototype.getAll = function() { }.bind(this)); }; +var defaultConnectorName; + /** - * Get access to a connector by name. If name is undefined then use the first - * registered connector as a default. + * Get access to a connector by name. If name is undefined then first try to + * use the same connector that we used last time, and if there was no last + * time, then just use the first registered connector as a default. */ Connectors.prototype.get = function(name) { if (name == null) { - name = Object.keys(this._registered)[0]; + name = (defaultConnectorName == null) ? + Object.keys(this._registered)[0] : + defaultConnectorName } + + defaultConnectorName = name; return this._registered[name]; }; From 32b000608b4bd1fea92eae964f1d1f423038b107 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:18:53 +0000 Subject: [PATCH 07/42] runat-1128988: Add json -> dom and json -> string converters Nothing fancy, but given JSON.stringify it would be rude not to. Signed-off-by: Joe Walker --- lib/gcli/converters/basic.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/gcli/converters/basic.js b/lib/gcli/converters/basic.js index fdb41d4c..2efd9abb 100644 --- a/lib/gcli/converters/basic.js +++ b/lib/gcli/converters/basic.js @@ -54,6 +54,14 @@ exports.items = [ return util.createElement(conversionContext.document, 'span'); } }, + { + item: 'converter', + from: 'json', + to: 'dom', + exec: function(json, conversionContext) { + return nodeFromDataToString(JSON.stringify(json), conversionContext); + } + }, { item: 'converter', from: 'number', @@ -71,5 +79,13 @@ exports.items = [ from: 'undefined', to: 'string', exec: function(data) { return ''; } + }, + { + item: 'converter', + from: 'json', + to: 'string', + exec: function(json, conversionContext) { + return JSON.stringify(json); + } } ]; From c8ccc9fa3af9f69657f03cb55d3c8d45f250d317 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:21:06 +0000 Subject: [PATCH 08/42] runat-1128988: Audit test skips for PhantomJS 2.0 Better comments for the existing skips and remove a skip that was no longer needed. Signed-off-by: Joe Walker --- lib/gcli/test/testCli2.js | 2 +- lib/gcli/test/testCompletion1.js | 2 +- lib/gcli/test/testFile.js | 5 +---- lib/gcli/test/testUnion.js | 4 ++-- lib/gcli/test/testUrl.js | 2 +- 5 files changed, 6 insertions(+), 9 deletions(-) diff --git a/lib/gcli/test/testCli2.js b/lib/gcli/test/testCli2.js index 1a8ecaf6..3b242336 100644 --- a/lib/gcli/test/testCli2.js +++ b/lib/gcli/test/testCli2.js @@ -581,7 +581,7 @@ exports.testNestedCommand = function(options) { } }, { - skipIf: options.isPhantomjs, + skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong setup: 'tsn x', check: { input: 'tsn x', diff --git a/lib/gcli/test/testCompletion1.js b/lib/gcli/test/testCompletion1.js index afcff226..1b1821f0 100644 --- a/lib/gcli/test/testCompletion1.js +++ b/lib/gcli/test/testCompletion1.js @@ -159,7 +159,7 @@ exports.testActivate = function(options) { } }, { - skipIf: options.isPhantomjs, + skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong setup: 'tsg d', check: { hints: ' [options] -> ccc' diff --git a/lib/gcli/test/testFile.js b/lib/gcli/test/testFile.js index c00dc4bf..78a434fa 100644 --- a/lib/gcli/test/testFile.js +++ b/lib/gcli/test/testFile.js @@ -23,10 +23,7 @@ var local = false; exports.testBasic = function(options) { return helpers.audit(options, [ { - // These tests require us to be using node directly or to be in - // PhantomJS connected to an execute enabled node server or to be in - // firefox. - skipRemainingIf: options.isPhantomjs || options.isFirefox, + skipRemainingIf: options.isFirefox, // No file implementation in Firefox setup: 'tsfile open /', check: { input: 'tsfile open /', diff --git a/lib/gcli/test/testUnion.js b/lib/gcli/test/testUnion.js index 8a27a0e1..b5b7a802 100644 --- a/lib/gcli/test/testUnion.js +++ b/lib/gcli/test/testUnion.js @@ -102,7 +102,7 @@ exports.testDefault = function(options) { } }, { - skipIf: options.isPhantomjs, // Phantom goes weird with predictions + skipIf: options.isPhantomjs, // PhantomJS gets predictions wrong setup: 'unionc1 5', check: { input: 'unionc1 5', @@ -136,7 +136,7 @@ exports.testDefault = function(options) { } }, { - skipRemainingIf: options.isPhantomjs, + skipIf: options.isPhantomjs, // PhantomJS URL type is broken setup: 'unionc2 on', check: { input: 'unionc2 on', diff --git a/lib/gcli/test/testUrl.js b/lib/gcli/test/testUrl.js index 54edee36..5a332b80 100644 --- a/lib/gcli/test/testUrl.js +++ b/lib/gcli/test/testUrl.js @@ -22,7 +22,7 @@ var helpers = require('./helpers'); exports.testDefault = function(options) { return helpers.audit(options, [ { - skipRemainingIf: options.isPhantomjs, + skipRemainingIf: options.isPhantomjs, // PhantomJS URL type is broken setup: 'urlc', check: { input: 'urlc', From 05b78be9c51ebf4c99d9ea1c63f4b57da3bc532a Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:22:19 +0000 Subject: [PATCH 09/42] runat-1128988: More robust error reporting If a promise rejected with null/undefined, then we previously made a bad thing worse. Now we protect ourselves. Signed-off-by: Joe Walker --- lib/gcli/testharness/examiner.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/gcli/testharness/examiner.js b/lib/gcli/testharness/examiner.js index e6d51438..edabd3c6 100644 --- a/lib/gcli/testharness/examiner.js +++ b/lib/gcli/testharness/examiner.js @@ -379,6 +379,14 @@ Test.prototype.run = function(options) { * Object.toString could be a lot better */ function toString(err) { + if (err === null) { + return 'null'; + } + + if (err === undefined) { + return 'undefined'; + } + // Convert err to a string if (typeof err === 'string') { return err; From 60323f6344f81b209100d4ad46cda0ea24c60de6 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:26:46 +0000 Subject: [PATCH 10/42] runat-1128988: Minor comment improvements Signed-off-by: Joe Walker --- lib/gcli/connectors/index.js | 4 +++- lib/gcli/types/delegate.js | 4 ++-- lib/gcli/types/setting.js | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 67915d16..175726da 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -146,8 +146,10 @@ exports.addLocalFunctions = function(specs, connection) { // Inject an 'exec' function into the commands, and the connection into // all the remote types specs.forEach(function(commandSpec) { - // + // HACK: Tack the connection to the command so we know how to remove it + // in removeRemoteItems() below commandSpec.connection = connection; + commandSpec.params.forEach(function(param) { param.type.connection = connection; }); diff --git a/lib/gcli/types/delegate.js b/lib/gcli/types/delegate.js index 50597f6b..65b4c4fb 100644 --- a/lib/gcli/types/delegate.js +++ b/lib/gcli/types/delegate.js @@ -87,8 +87,8 @@ exports.items = [ return Promise.resolve(type); }, - // DelegateType is designed to be inherited from, so DelegateField needs a way - // to check if something works like a delegate without using 'name' + // DelegateType is designed to be inherited from, so DelegateField needs a + // way to check if something works like a delegate without using 'name' isDelegate: true, // Technically we perhaps should proxy this, except that properties are diff --git a/lib/gcli/types/setting.js b/lib/gcli/types/setting.js index 509b9514..26c6f406 100644 --- a/lib/gcli/types/setting.js +++ b/lib/gcli/types/setting.js @@ -25,12 +25,15 @@ exports.items = [ cacheable: true, lookup: function(context) { var settings = context.system.settings; + + // Lazily add a settings.onChange listener to clear the cache if (!this._registeredListener) { settings.onChange.add(function(ev) { this.clearCache(); }, this); this._registeredListener = true; } + return settings.getAll().map(function(setting) { return { name: setting.name, value: setting }; }); From f59f82f70a771ecbc5dd43d653671eba144cdef7 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:28:12 +0000 Subject: [PATCH 11/42] runat-1128988: Check for hidden options in a function Checking that an option is hidden involves messing in properties on the value right now (which isn't a good idea really) we really should be marking that on the option, so this encapsulates the problem Signed-off-by: Joe Walker --- lib/gcli/types/selection.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/gcli/types/selection.js b/lib/gcli/types/selection.js index 5ec3d38f..c9b79604 100644 --- a/lib/gcli/types/selection.js +++ b/lib/gcli/types/selection.js @@ -227,7 +227,7 @@ exports.findPredictions = function(arg, lookup) { } // Exact hidden matches. If 'hidden: true' then we only allow exact matches - // All the tests after here check that !option.value.hidden + // All the tests after here check that !isHidden(option) for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; if (option.name === arg.text) { @@ -238,7 +238,7 @@ exports.findPredictions = function(arg, lookup) { // Start with prefix matching for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; - if (option._gcliLowerName.indexOf(match) === 0 && !option.value.hidden) { + if (option._gcliLowerName.indexOf(match) === 0 && !isHidden(option)) { if (predictions.indexOf(option) === -1) { predictions.push(option); } @@ -249,7 +249,7 @@ exports.findPredictions = function(arg, lookup) { if (predictions.length < (maxPredictions / 2)) { for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { option = lookup[i]; - if (option._gcliLowerName.indexOf(match) !== -1 && !option.value.hidden) { + if (option._gcliLowerName.indexOf(match) !== -1 && !isHidden(option)) { if (predictions.indexOf(option) === -1) { predictions.push(option); } @@ -261,7 +261,7 @@ exports.findPredictions = function(arg, lookup) { if (predictions.length === 0) { var names = []; lookup.forEach(function(opt) { - if (!opt.value.hidden) { + if (!isHidden(opt)) { names.push(opt.name); } }); @@ -306,11 +306,21 @@ exports.convertPredictions = function(arg, predictions) { Promise.resolve(predictions)); }; +/** + * Checking that an option is hidden involves messing in properties on the + * value right now (which isn't a good idea really) we really should be marking + * that on the option, so this encapsulates the problem + */ +function isHidden(option) { + return option.hidden === true || + (option.value != null && option.value.hidden); +} + SelectionType.prototype.getBlank = function(context) { var predictFunc = function(context2) { return Promise.resolve(this.getLookup(context2)).then(function(lookup) { return lookup.filter(function(option) { - return !option.value.hidden; + return !isHidden(option); }).slice(0, Conversion.maxPredictions - 1); }); }.bind(this); From fc6a0326a8b942cd1306ba791785f6a42dec3b53 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:29:29 +0000 Subject: [PATCH 12/42] runat-1128988: Fix error handling in websocket server We didn't catch rejected promises, and just silently failed to reply. Also we're less noisy about logging what's going on now. Signed-off-by: Joe Walker --- lib/gcli/commands/server/server.js | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/gcli/commands/server/server.js b/lib/gcli/commands/server/server.js index 746d5cdb..3e26a33f 100644 --- a/lib/gcli/commands/server/server.js +++ b/lib/gcli/commands/server/server.js @@ -249,31 +249,41 @@ var websocket = { var remoter = new Remoter(requisition); remoter.addListener(function(name, data) { - console.log('EMIT ' + name + ' ' + debugStr(data, 30)); + // console.log('EMIT ' + name + ' ' + debugStr(data, 30)); socket.emit('event', { name: name, data: data }); }); Object.keys(remoter.exposed).forEach(function(command) { socket.on(command, function(request) { - console.log('SOCKET ' + command + ' ' + debugStr(request.data, 30)); + // Handle errors from exceptions an promise rejections + var onError = function(err) { + console.trace(); + console.log('SOCKET ' + command + + '(' + debugStr(request.data, 30) + ') Exception'); + console.error(err); + + socket.emit('reply', { + id: request.id, + exception: '' + err + }); + }; try { var func = remoter.exposed[command]; var reply = func.call(remoter, request.data); Promise.resolve(reply).then(function(data) { + console.log('SOCKET ' + command + + '(' + debugStr(request.data, 30) + ') → ' + + debugStr(data, 20) + ')'); + socket.emit('reply', { id: request.id, reply: data }); - }); + }, onError); } catch (ex) { - console.trace(); - console.error(ex); - socket.emit('reply', { - id: request.id, - exception: ex.toString() - }); + onError(ex); } }); }); From a8f6a6a379d5696b2623f622ad3e013ba3e58e27 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:32:45 +0000 Subject: [PATCH 13/42] runat-1128988: Don't pass terminal to addDebugAids terminal is available on the options object, so it's not needed. Signed-off-by: Joe Walker --- lib/gcli/test/index.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/gcli/test/index.js b/lib/gcli/test/index.js index 33d18359..ff130650 100644 --- a/lib/gcli/test/index.js +++ b/lib/gcli/test/index.js @@ -28,9 +28,7 @@ require('./suite'); /** * Some tricks to make studying the command line state easier */ -var addDebugAids = exports.addDebugAids = function(options, terminal) { - var requisition = terminal.language.requisition; - +var addDebugAids = exports.addDebugAids = function(options) { window.createDebugCheck = function() { helpers._createDebugCheck(options).then(function() { // Don't inline this - chrome console suckage @@ -40,7 +38,7 @@ var addDebugAids = exports.addDebugAids = function(options, terminal) { window.summaryJson = function() { var args = [ 'Requisition: ' ]; - var summary = requisition._summaryJson; + var summary = options.terminal.language.requisition._summaryJson; Object.keys(summary).forEach(function(name) { args.push(' ' + name + '='); args.push(summary[name]); @@ -48,8 +46,8 @@ var addDebugAids = exports.addDebugAids = function(options, terminal) { console.log.apply(console, args); console.log('Focus: ' + - 'tooltip=', terminal.focusManager._shouldShowTooltip(), - 'output=', terminal.focusManager._shouldShowOutput()); + 'tooltip=', options.terminal.focusManager._shouldShowTooltip(), + 'output=', options.terminal.focusManager._shouldShowOutput()); }; document.addEventListener('keyup', function(ev) { @@ -83,7 +81,7 @@ exports.run = function(options) { options.isRemote = (options.connection != null); options.hideExec = true; - addDebugAids(options, options.terminal); + addDebugAids(options); // phantom-test.js does phantom.exit() on `document.complete = true` var closeIfPhantomJs = function() { From 8096a5dc875566c8a35935a2d94b2408d09bf626 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:37:47 +0000 Subject: [PATCH 14/42] runat-1128988: Make a system immutable once created I'm not sure why we ever thought a post-creation mutation was required but it looks from the code that it once was. Not any more however. Signed-off-by: Joe Walker --- lib/gcli/api.js | 23 +++++++++++------------ lib/gcli/connectors/direct.js | 3 --- lib/gcli/connectors/index.js | 6 ------ 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/gcli/api.js b/lib/gcli/api.js index c2d0f71e..e648e6c5 100644 --- a/lib/gcli/api.js +++ b/lib/gcli/api.js @@ -28,14 +28,19 @@ var Types = require('./types/types').Types; /** * This is the heart of the API that we expose to the outside */ -exports.createSystem = function() { +exports.createSystem = function(options) { + options = options || {}; + var location = options.location; + // The plural/singular thing may make you want to scream, but it allows us + // to say components[getItemType(item)], so a lookup here (and below) saves + // multiple lookups in the middle of the code var components = { - connector: new Connectors(), - converter: new Converters(), - field: new Fields(), - language: new Languages(), - type: new Types() + connector: options.connectors || new Connectors(), + converter: options.converters || new Converters(), + field: options.fields || new Fields(), + language: options.languages || new Languages(), + type: options.types || new Types() }; components.setting = new Settings(components.type); components.command = new Commands(components.type); @@ -174,7 +179,6 @@ exports.createSystem = function() { Object.defineProperty(api, 'commands', { get: function() { return components.command; }, - set: function(commands) { components.command = commands; }, enumerable: true }); @@ -205,11 +209,6 @@ exports.createSystem = function() { Object.defineProperty(api, 'types', { get: function() { return components.type; }, - set: function(types) { - components.type = types; - components.command.types = types; - components.setting.types = types; - }, enumerable: true }); diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index f9dc380d..c1cfcc26 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -25,9 +25,6 @@ var Promise = require('../util/promise').Promise; var Remoter = require('./remoted').Remoter; var Connection = require('./connectors').Connection; -system.types = new Types(); -system.commands = new Commands(system.types); - var items = [ require('../types/delegate').items, require('../types/selection').items, diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 175726da..2dfa77b5 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -109,12 +109,6 @@ exports.connect = function(options) { var system = api.createSystem(); - // Ugly hack, to aid testing - exports.api = system; - - options.types = system.types = new Types(); - options.commands = system.commands = new Commands(system.types); - system.addItems(items); system.addItems(requiredConverters); From 8292a265580ce7ffa7912da34c0ff66c2a986943 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:41:51 +0000 Subject: [PATCH 15/42] runat-1128988: Use a blank type when we can't resolve the delegate type Delegated types are used when we're not sure a load time what the type should be. An example is the 'pref set' command where the new pref value is determined by the pref being set. The addition of remoting made this asynchronous, but we've not had time to make all the required adjustments yet. This just assumes a 'blank' type if we can't ask. Signed-off-by: Joe Walker --- lib/gcli/types/delegate.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/lib/gcli/types/delegate.js b/lib/gcli/types/delegate.js index 65b4c4fb..3b63eb24 100644 --- a/lib/gcli/types/delegate.js +++ b/lib/gcli/types/delegate.js @@ -29,14 +29,6 @@ exports.items = [ item: 'type', name: 'delegate', - constructor: function() { - if (typeof this.delegateType !== 'function' && - typeof this.delegateType !== 'string') { - throw new Error('Instances of DelegateType need typeSpec.delegateType' + - ' to be a function that returns a type'); - } - }, - getSpec: function(commandName, paramName) { return { name: 'delegate', @@ -47,9 +39,7 @@ exports.items = [ // Child types should implement this method to return an instance of the type // that should be used. If no type is available, or some sort of temporary // placeholder is required, BlankType can be used. - delegateType: function(context) { - throw new Error('Not implemented'); - }, + delegateType: undefined, stringify: function(value, context) { return this.getType(context).then(function(delegated) { @@ -80,6 +70,10 @@ exports.items = [ }, getType: function(context) { + if (this.delegateType === undefined) { + return Promise.resolve(this.types.createType('blank')); + } + var type = this.delegateType(context); if (typeof type.parse !== 'function') { type = this.types.createType(type); From 8246a7760f95993506d14d230dbe09641da21f1e Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 16:52:00 +0000 Subject: [PATCH 16/42] runat-1128988: Strip values from selection lookups There is no reason that a value should be JSONable, and in some cases they're not, so strip them out. Practically we shouldn't need the values on the other side of the remote interface anyway. Signed-off-by: Joe Walker --- lib/gcli/connectors/remoted.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index 91e6900f..a3ba4c2c 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -206,11 +206,19 @@ Remoter.prototype.exposed = { commandName + '\''); } + var context = this.requisition.executionContext; + switch (action) { case 'lookup': - return type.lookup(this.requisition.executionContext); + return type.lookup(context).map(function(info) { + // lookup returns an array of objects with name/value properties and + // the values might not be JSONable, so remove them + return { name: info.name }; + }); + case 'data': - return type.data(this.requisition.executionContext); + return type.data(context); + default: throw new Error('Action must be either \'lookup\' or \'data\''); } From f525d597b0daa7571d3b43977a21ed7a90e6a848 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 17:46:40 +0000 Subject: [PATCH 17/42] runat-1128988: Report rejected promises Chance some places where promises were being dropped on the floor. Signed-off-by: Joe Walker --- lib/gcli/languages/command.js | 4 ++-- lib/gcli/languages/languages.js | 12 +++++++----- lib/gcli/ui/terminal.js | 14 +++++++++++--- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/gcli/languages/command.js b/lib/gcli/languages/command.js index f423a8e4..bc9f4381 100644 --- a/lib/gcli/languages/command.js +++ b/lib/gcli/languages/command.js @@ -184,7 +184,7 @@ var commandLanguage = exports.commandLanguage = { var isNew = (this.assignment !== newAssignment); this.assignment = newAssignment; - this.terminal.updateCompletion(); + this.terminal.updateCompletion().then(null, util.errorHandler); if (isNew) { this.updateHints(); @@ -286,7 +286,7 @@ var commandLanguage = exports.commandLanguage = { } this.terminal.history.add(input); - this.terminal.unsetChoice(); + this.terminal.unsetChoice().then(null, util.errorHandler); return this.requisition.exec().then(function() { this.textChanged(); diff --git a/lib/gcli/languages/languages.js b/lib/gcli/languages/languages.js index 49e0f1ec..738c7f94 100644 --- a/lib/gcli/languages/languages.js +++ b/lib/gcli/languages/languages.js @@ -51,8 +51,9 @@ var baseLanguage = { }, handleTab: function() { - this.terminal.unsetChoice(); - return RESOLVED; + return this.terminal.unsetChoice().then(function() { + return RESOLVED; + }, util.errorHandler); }, handleInput: function(input) { @@ -62,8 +63,9 @@ var baseLanguage = { }.bind(this)); } - this.terminal.unsetChoice(); - return RESOLVED; + return this.terminal.unsetChoice().then(function() { + return RESOLVED; + }, util.errorHandler); }, handleReturn: function(input) { @@ -80,7 +82,7 @@ var baseLanguage = { this.focusManager.outputted(); - this.terminal.unsetChoice(); + this.terminal.unsetChoice().then(null, util.errorHandler); this.terminal.inputElement.value = ''; }.bind(this)); }, diff --git a/lib/gcli/ui/terminal.js b/lib/gcli/ui/terminal.js index fb2bbaa6..0c68e9a1 100644 --- a/lib/gcli/ui/terminal.js +++ b/lib/gcli/ui/terminal.js @@ -150,7 +150,7 @@ Terminal.prototype._init = function(system, options, terminalCss, terminalHtml) this.focusManager.addMonitoredElement(this.tooltipElement, 'tooltip'); this.focusManager.addMonitoredElement(this.inputElement, 'input'); - this.onInputChange.add(this.updateCompletion, this); + this.onInputChange.add(this._updateCompletionWithErrorHandler, this); host.script.onOutput.add(this.onOutput); @@ -185,7 +185,7 @@ Terminal.prototype.destroy = function() { this.field.onFieldChange.remove(this.fieldChanged, this); this.field.destroy(); - this.onInputChange.remove(this.updateCompletion, this); + this.onInputChange.remove(this._updateCompletionWithErrorHandler, this); // Remove the output elements so they free the event handers util.clearElement(this.displayElement); @@ -265,7 +265,7 @@ Terminal.prototype._updateLanguage = function(language) { } this.language.updateHints(); - this.updateCompletion(); + this.updateCompletion().then(null, util.errorHandler); this.promptElement.innerHTML = this.language.prompt; }; @@ -584,6 +584,14 @@ Terminal.prototype.updateCompletion = function() { }.bind(this)); }; +/** + * Call updateCompletion, but log if something is wrong. To be called by + * event handlers that can't react to rejected promises. + */ +Terminal.prototype._updateCompletionWithErrorHandler = function() { + this.updateCompletion().then(null, util.errorHandler); +}; + /** * The terminal acts on UP/DOWN if there is a menu showing */ From 2f57fa975754fdcd8abb2d8956d6939975dc1abb Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 17:51:24 +0000 Subject: [PATCH 18/42] runat-1128988: Update gcli/index API to not include system Now systems are created explicitly with createSystem, and the startup sequence is less magic. Signed-off-by: Joe Walker --- index.html | 18 ++++++++---------- lib/gcli/connectors/direct.js | 12 +++++++----- lib/gcli/connectors/index.js | 17 ++++++++++------- lib/gcli/index.js | 32 +++----------------------------- lib/gcli/test/index.js | 25 +++++++++++++------------ lib/gcli/ui/terminal.js | 1 + remote.html | 6 +++--- 7 files changed, 45 insertions(+), 66 deletions(-) diff --git a/index.html b/index.html index 6f1cc562..ed7eea48 100644 --- a/index.html +++ b/index.html @@ -27,17 +27,15 @@ var modules = [ 'gcli/index', 'gcli/demo', 'gcli/test/index' ]; require(modules, function(gcli, demo, test) { - // Add demo commands. You'll probably want to replace this with - // your own set of commands - gcli.addItems(demo.items); + // Add the commands/types/converters as required + var system = gcli.createSystem(); + system.addItems(gcli.items); // The basic set that most people need + system.addItems(demo.items); // Demo commands - // To run commands remotely, - // Hook GCLI into this page, using gcli-root above - var options = {}; - gcli.createTerminal(options).then(function() { - // Run the unit test at each startup. - test.run(options); - }).then(null, console.error); + gcli.createTerminal(system).then(function(terminal) { + terminal.language.showIntro(); // Intro text + test.run(terminal); // Run the unit test at each startup + }).then(null, console.error.bind(console)); }); diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index c1cfcc26..85362fdf 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -16,7 +16,7 @@ 'use strict'; -var system = require('../api').createSystem(); +var createSystem = require('../api').createSystem; var Commands = require('../commands/commands').Commands; var Types = require('../types/types').Types; var Requisition = require('../cli').Requisition; @@ -63,10 +63,6 @@ var items = [ ].reduce(function(prev, curr) { return prev.concat(curr); }, []); -system.addItems(items); - -var requisition = new Requisition(system); - exports.items = [ { // Communicate with a 'remote' server that isn't remote at all @@ -81,6 +77,12 @@ exports.items = [ function DirectConnection() { this._emit = this._emit.bind(this); + + // This is the 'server' + var system = createSystem(); + system.addItems(items); + var requisition = new Requisition(system); + this.remoter = new Remoter(requisition); this.remoter.addListener(this._emit); } diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 2dfa77b5..53e80cf4 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -16,7 +16,7 @@ 'use strict'; -var api = require('../api'); +var createSystem = require('../api').createSystem; var Commands = require('../commands/commands').Commands; var Types = require('../types/types').Types; @@ -81,7 +81,8 @@ var items = [ /** * These are the commands stored on the remote side that have converters which - * we'll need to present the data + * we'll need to present the data. Ideally connection.specs would transfer + * these, that doesn't happen yet so we add them manually */ var requiredConverters = [ require('../cli').items, @@ -107,21 +108,20 @@ var requiredConverters = [ exports.connect = function(options) { options = options || {}; - var system = api.createSystem(); - + var system = createSystem(); system.addItems(items); system.addItems(requiredConverters); var connector = system.connectors.get(options.connector); return connector.connect(options.url).then(function(connection) { - options.connection = connection; + system.connection = connection; connection.on('commandsChanged', function(specs) { exports.addItems(system, specs, connection); }); return connection.call('specs').then(function(specs) { exports.addItems(system, specs, connection); - return connection; + return system; }); }); }; @@ -144,8 +144,11 @@ exports.addLocalFunctions = function(specs, connection) { // in removeRemoteItems() below commandSpec.connection = connection; + // TODO: removeRemoteItems() doesn't remove types, so do we need this? commandSpec.params.forEach(function(param) { - param.type.connection = connection; + if (typeof param.type !== 'string') { + param.type.connection = connection; + } }); if (!commandSpec.isParent) { diff --git a/lib/gcli/index.js b/lib/gcli/index.js index e34c22a2..13543ad6 100644 --- a/lib/gcli/index.js +++ b/lib/gcli/index.js @@ -16,7 +16,7 @@ 'use strict'; -var api = require('./api'); +var createSystem = require('./api').createSystem; var Terminal = require('./ui/terminal').Terminal; // Patch-up old browsers @@ -76,32 +76,6 @@ exports.items = [ // No commands in the basic set ].reduce(function(prev, curr) { return prev.concat(curr); }, []); -var system = api.createSystem(); +exports.createSystem = createSystem; -// Export the system API by adding it to our exports -Object.keys(system).forEach(function(key) { - exports[key] = system[key]; -}); - -system.addItems(exports.items); - -/** - * createTerminal() calls 'Terminal.create()' but returns an object which - * exposes a much restricted set of functions rather than all those exposed - * by Terminal. - * This allows for robust testing without exposing too many internals. - * @param options See Terminal.create() for a description of the available - * options. - */ -exports.createTerminal = function(options) { - options = options || {}; - if (options.settings != null) { - system.settings.setDefaults(options.settings); - } - - return Terminal.create(system, options).then(function(terminal) { - options.terminal = terminal; - terminal.language.showIntro(); - return terminal; - }); -}; +exports.createTerminal = Terminal.create.bind(Terminal); diff --git a/lib/gcli/test/index.js b/lib/gcli/test/index.js index ff130650..a9ee2f1a 100644 --- a/lib/gcli/test/index.js +++ b/lib/gcli/test/index.js @@ -69,17 +69,18 @@ var addDebugAids = exports.addDebugAids = function(options) { * - Runs the unit tests automatically on startup * - Registers a 'test' command to re-run the unit tests */ -exports.run = function(options) { - var requisition = options.terminal.language.requisition; - - options.window = window; - options.automator = createTerminalAutomator(options.terminal); - options.requisition = requisition; - options.isNode = false; - options.isFirefox = false; - options.isPhantomjs = (window.navigator.userAgent.indexOf('hantom') !== -1); - options.isRemote = (options.connection != null); - options.hideExec = true; +exports.run = function(terminal) { + var options = { + terminal: terminal, + window: window, + automator: createTerminalAutomator(terminal), + requisition: terminal.language.requisition, + isNode: false, + isFirefox: false, + isPhantomjs: (window.navigator.userAgent.indexOf('hantom') !== -1), + isRemote: (terminal.system.connection != null), + hideExec: true + }; addDebugAids(options); @@ -92,7 +93,7 @@ exports.run = function(options) { // PhantomJS may tell us to tell the server to shutdown if (window.location.href.indexOf('shutdown=true') > 0) { - shutdownServer(requisition).then(closeIfPhantomJs, closeIfPhantomJs); + shutdownServer(terminal.language.requisition).then(closeIfPhantomJs, closeIfPhantomJs); return; } diff --git a/lib/gcli/ui/terminal.js b/lib/gcli/ui/terminal.js index 0c68e9a1..8a3abbbd 100644 --- a/lib/gcli/ui/terminal.js +++ b/lib/gcli/ui/terminal.js @@ -47,6 +47,7 @@ function Terminal() { * - document */ Terminal.create = function(system, options) { + options = options || {}; if (resourcesPromise == null) { resourcesPromise = Promise.all([ host.staticRequire(module, './terminal.css'), diff --git a/remote.html b/remote.html index 6c586514..29530b41 100644 --- a/remote.html +++ b/remote.html @@ -28,9 +28,9 @@ require([ 'gcli/index', 'gcli/test/index', 'gcli/connectors/index' ], function(gcli, test, cnx) { var options = { connector: 'websocket' }; - cnx.connect(options).then(function() { - gcli.createTerminal(options).then(function() { - test.run(options); + cnx.connect(options).then(function(system) { + gcli.createTerminal(system).then(function(terminal) { + test.run(terminal); }); }).then(null, console.error.bind(console)); }); From 9c82b3075aea5e6baa1c4a193de7a292bbe7698a Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 18:00:07 +0000 Subject: [PATCH 19/42] runat-1128988: Rename api.js to system.js Signed-off-by: Joe Walker --- gcli.js | 2 +- lib/gcli/connectors/direct.js | 2 +- lib/gcli/connectors/index.js | 2 +- lib/gcli/index.js | 2 +- lib/gcli/{api.js => system.js} | 18 +++++++++--------- 5 files changed, 13 insertions(+), 13 deletions(-) rename lib/gcli/{api.js => system.js} (94%) diff --git a/gcli.js b/gcli.js index 5708a0d9..5aac4f8f 100644 --- a/gcli.js +++ b/gcli.js @@ -19,7 +19,7 @@ exports.gcliHome = __dirname; -var system = require('./lib/gcli/api').createSystem(); +var system = require('./lib/gcli/system').createSystem(); /* * GCLI is built from a number of components (called items) composed as diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index 85362fdf..edb784b1 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -16,7 +16,7 @@ 'use strict'; -var createSystem = require('../api').createSystem; +var createSystem = require('../system').createSystem; var Commands = require('../commands/commands').Commands; var Types = require('../types/types').Types; var Requisition = require('../cli').Requisition; diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 53e80cf4..e375a661 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -16,7 +16,7 @@ 'use strict'; -var createSystem = require('../api').createSystem; +var createSystem = require('../system').createSystem; var Commands = require('../commands/commands').Commands; var Types = require('../types/types').Types; diff --git a/lib/gcli/index.js b/lib/gcli/index.js index 13543ad6..e630ac7d 100644 --- a/lib/gcli/index.js +++ b/lib/gcli/index.js @@ -16,7 +16,7 @@ 'use strict'; -var createSystem = require('./api').createSystem; +var createSystem = require('./system').createSystem; var Terminal = require('./ui/terminal').Terminal; // Patch-up old browsers diff --git a/lib/gcli/api.js b/lib/gcli/system.js similarity index 94% rename from lib/gcli/api.js rename to lib/gcli/system.js index e648e6c5..d1da29b3 100644 --- a/lib/gcli/api.js +++ b/lib/gcli/system.js @@ -119,7 +119,7 @@ exports.createSystem = function(options) { var pendingChanges = false; - var api = { + var system = { addItems: function(items) { items.forEach(addItem); }, @@ -177,40 +177,40 @@ exports.createSystem = function(options) { } }; - Object.defineProperty(api, 'commands', { + Object.defineProperty(system, 'commands', { get: function() { return components.command; }, enumerable: true }); - Object.defineProperty(api, 'connectors', { + Object.defineProperty(system, 'connectors', { get: function() { return components.connector; }, enumerable: true }); - Object.defineProperty(api, 'converters', { + Object.defineProperty(system, 'converters', { get: function() { return components.converter; }, enumerable: true }); - Object.defineProperty(api, 'fields', { + Object.defineProperty(system, 'fields', { get: function() { return components.field; }, enumerable: true }); - Object.defineProperty(api, 'languages', { + Object.defineProperty(system, 'languages', { get: function() { return components.language; }, enumerable: true }); - Object.defineProperty(api, 'settings', { + Object.defineProperty(system, 'settings', { get: function() { return components.setting; }, enumerable: true }); - Object.defineProperty(api, 'types', { + Object.defineProperty(system, 'types', { get: function() { return components.type; }, enumerable: true }); - return api; + return system; }; From 304a18c7a61a56ba2185aedf39aefcf373cda00d Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Feb 2015 18:01:09 +0000 Subject: [PATCH 20/42] runat-1128988: Mark tests that fail when run remotely Signed-off-by: Joe Walker --- lib/gcli/test/testExec.js | 2 +- lib/gcli/test/testPref1.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/gcli/test/testExec.js b/lib/gcli/test/testExec.js index 30b0e263..018e2b16 100644 --- a/lib/gcli/test/testExec.js +++ b/lib/gcli/test/testExec.js @@ -399,7 +399,7 @@ exports.testExecNode = function(options) { return helpers.audit(options, [ { - skipIf: options.isNoDom, + skipIf: options.isNoDom || options.isRemote, setup: 'tse :root', check: { input: 'tse :root', diff --git a/lib/gcli/test/testPref1.js b/lib/gcli/test/testPref1.js index b227a8a1..4082a301 100644 --- a/lib/gcli/test/testPref1.js +++ b/lib/gcli/test/testPref1.js @@ -135,6 +135,7 @@ exports.testPrefSetStatus = function(options) { } }, { + skipIf: options.isRemote, setup: 'pref set tempTBool 4', check: { typed: 'pref set tempTBool 4', From 118be5414fdd8635d064ffa8f3f23c0c42e1e29b Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 11:17:55 +0000 Subject: [PATCH 21/42] runat-1128988: Rationalize commands 1: Remove uncared for commands Several commands were fairly pointless or in various stages of broken so we removed everything that wasn't good, and included everything that was. Signed-off-by: Joe Walker --- gcli.js | 9 +- lib/gcli/commands/demo/bugs.js | 134 ---------------- lib/gcli/commands/demo/edit.js | 39 ----- lib/gcli/commands/demo/git.js | 276 -------------------------------- lib/gcli/commands/demo/hg.js | 81 ---------- lib/gcli/commands/demo/theme.js | 96 ----------- 6 files changed, 2 insertions(+), 633 deletions(-) delete mode 100644 lib/gcli/commands/demo/bugs.js delete mode 100644 lib/gcli/commands/demo/edit.js delete mode 100644 lib/gcli/commands/demo/git.js delete mode 100644 lib/gcli/commands/demo/hg.js delete mode 100644 lib/gcli/commands/demo/theme.js diff --git a/gcli.js b/gcli.js index 5aac4f8f..79d9ce00 100644 --- a/gcli.js +++ b/gcli.js @@ -39,7 +39,7 @@ var items = [ require('./lib/gcli/cli').items, require('./lib/gcli/commands/clear').items, - // require('./lib/gcli/commands/connect').items, + require('./lib/gcli/commands/connect').items, require('./lib/gcli/commands/context').items, require('./lib/gcli/commands/exec').items, require('./lib/gcli/commands/global').items, @@ -52,14 +52,9 @@ var items = [ require('./lib/gcli/commands/test').items, require('./lib/gcli/commands/demo/alert').items, - // require('./lib/gcli/commands/demo/bugs').items, - // require('./lib/gcli/commands/demo/demo').items, + require('./lib/gcli/commands/demo/demo').items, require('./lib/gcli/commands/demo/echo').items, - // require('./lib/gcli/commands/demo/edit').items, - // require('./lib/gcli/commands/demo/git').items, - // require('./lib/gcli/commands/demo/hg').items, require('./lib/gcli/commands/demo/sleep').items, - // require('./lib/gcli/commands/demo/theme').items, require('./lib/gcli/commands/server/exit').items, require('./lib/gcli/commands/server/firefox').items, diff --git a/lib/gcli/commands/demo/bugs.js b/lib/gcli/commands/demo/bugs.js deleted file mode 100644 index ef1ce409..00000000 --- a/lib/gcli/commands/demo/bugs.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -var Promise = require('../../util/promise').Promise; - -exports.items = [ - { - item: 'converter', - from: 'bugz', - to: 'view', - exec: function(bugz, context) { - return { - html: - '
\n' + - '

\n' + - ' Open GCLI meta-bugs\n' + - ' (i.e. this search):\n' + - '

\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
IDMilestonePriSummary
${bug.id}${bug.target_milestone}${bug.priority}${bug.summary}
\n' + - '
', - data: bugz - }; - } - }, - { - item: 'command', - name: 'bugz', - returnType: 'bugz', - description: 'List the GCLI bugs open in Bugzilla', - exec: function(args, context) { - return queryBugzilla(args, context).then(filterReply); - } - } -]; - -/** - * Simple wrapper for querying bugzilla. - * @see https://wiki.mozilla.org/Bugzilla:REST_API - * @see https://wiki.mozilla.org/Bugzilla:REST_API:Search - * @see http://www.bugzilla.org/docs/developer.html - * @see https://harthur.wordpress.com/2011/03/31/bz-js/ - * @see https://github.com/harthur/bz.js - */ -function queryBugzilla(args, context) { - return new Promise(function(resolve, reject) { - var url = 'https://api-dev.bugzilla.mozilla.org/1.1/bug?' + - 'short_desc=GCLI' + - '&short_desc_type=allwords' + - '&bug_status=UNCONFIRMED' + - '&bug_status=NEW' + - '&bug_status=ASSIGNED' + - '&bug_status=REOPENED'; - - var req = new XMLHttpRequest(); - req.open('GET', url, true); - req.setRequestHeader('Accept', 'application/json'); - req.setRequestHeader('Content-type', 'application/json'); - req.onreadystatechange = function(event) { - if (req.readyState == 4) { - if (req.status >= 300 || req.status < 200) { - reject('Error: ' + JSON.stringify(req)); - return; - } - - try { - var json = JSON.parse(req.responseText); - if (json.error) { - reject('Error: ' + json.error.message); - } - else { - resolve(json); - } - } - catch (ex) { - reject('Invalid response: ' + ex + ': ' + req.responseText); - } - } - }; - req.send(); - }); -} - -/** - * Filter the output from Bugzilla for display - */ -function filterReply(json) { - json.bugs.forEach(function(bug) { - if (bug.target_milestone === '---') { - bug.target_milestone = 'Future'; - } - }); - - json.bugs.sort(function(bug1, bug2) { - var ms = bug1.target_milestone.localeCompare(bug2.target_milestone); - if (ms !== 0) { - return ms; - } - return bug1.priority.localeCompare(bug2.priority); - }); - - return json; -} diff --git a/lib/gcli/commands/demo/edit.js b/lib/gcli/commands/demo/edit.js deleted file mode 100644 index 62bb0883..00000000 --- a/lib/gcli/commands/demo/edit.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -exports.items = [ - { - item: 'command', - name: 'edit', - description: 'Edit a file', - params: [ - { - name: 'resource', - type: { name: 'resource', include: 'text/css' }, - description: 'The resource to edit' - } - ], - returnType: 'html', - exec: function(args, context) { - return args.resource.loadContents().then(function(data) { - return '

This is just a demo

' + - ''; - }); - } - } -]; diff --git a/lib/gcli/commands/demo/git.js b/lib/gcli/commands/demo/git.js deleted file mode 100644 index 63266dc3..00000000 --- a/lib/gcli/commands/demo/git.js +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -exports.items = [ - { - // commitObject really needs some smarts, but for now it is a clone of string - item: 'type', - name: 'commitObject', - parent: 'string' - }, - { - // existingFile really needs some smarts, but for now it is a clone of string - item: 'type', - name: 'existingFile', - parent: 'string' - }, - { - // Parent 'git' command - item: 'command', - name: 'git', - description: 'Distributed revision control in a browser', - manual: 'Git is a fast, scalable, distributed revision control system' + - ' with an unusually rich command set that provides both' + - ' high-level operations and full access to internals.' - }, - { - // 'git add' command - item: 'command', - name: 'git add', - description: 'Add file contents to the index', - manual: 'This command updates the index using the current content found in the working tree, to prepare the content staged for the next commit. It typically adds the current content of existing paths as a whole, but with some options it can also be used to add content with only part of the changes made to the working tree files applied, or remove paths that do not exist in the working tree anymore.' + - '
The "index" holds a snapshot of the content of the working tree, and it is this snapshot that is taken as the contents of the next commit. Thus after making any changes to the working directory, and before running the commit command, you must use the add command to add any new or modified files to the index.' + - '
This command can be performed multiple times before a commit. It only adds the content of the specified file(s) at the time the add command is run; if you want subsequent changes included in the next commit, then you must run git add again to add the new content to the index.' + - '
The git status command can be used to obtain a summary of which files have changes that are staged for the next commit.' + - '
The git add command will not add ignored files by default. If any ignored files were explicitly specified on the command line, git add will fail with a list of ignored files. Ignored files reached by directory recursion or filename globbing performed by Git (quote your globs before the shell) will be silently ignored. The git add command can be used to add ignored files with the -f (force) option.' + - '
Please see git-commit(1) for alternative ways to add content to a commit.', - params: [ - { - name: 'filepattern', - type: { name: 'array', subtype: 'string' }, - description: 'Files to add', - manual: 'Fileglobs (e.g. *.c) can be given to add all matching files. Also a leading directory name (e.g. dir to add dir/file1 and dir/file2) can be given to add all files in the directory, recursively.' - }, - { - group: 'Common Options', - params: [ - { - name: 'all', - short: 'a', - type: 'boolean', - description: 'All (unignored) files', - manual: 'That means that it will find new files as well as staging modified content and removing files that are no longer in the working tree.' - }, - { - name: 'verbose', - short: 'v', - type: 'boolean', - description: 'Verbose output' - }, - { - name: 'dry-run', - short: 'n', - type: 'boolean', - description: 'Dry run', - manual: 'Don\'t actually add the file(s), just show if they exist and/or will be ignored.' - }, - { - name: 'force', - short: 'f', - type: 'boolean', - description: 'Allow ignored files', - manual: 'Allow adding otherwise ignored files.' - } - ] - }, - { - group: 'Advanced Options', - params: [ - { - name: 'update', - short: 'u', - type: 'boolean', - description: 'Match only files already added', - manual: 'That means that it will never stage new files, but that it will stage modified new contents of tracked files and that it will remove files from the index if the corresponding files in the working tree have been removed.
If no is given, default to "."; in other words, update all tracked files in the current directory and its subdirectories.' - }, - { - name: 'refresh', - type: 'boolean', - description: 'Refresh only (don\'t add)', - manual: 'Don\'t add the file(s), but only refresh their stat() information in the index.' - }, - { - name: 'ignore-errors', - type: 'boolean', - description: 'Ignore errors', - manual: 'If some files could not be added because of errors indexing them, do not abort the operation, but continue adding the others. The command shall still exit with non-zero status.' - }, - { - name: 'ignore-missing', - type: 'boolean', - description: 'Ignore missing', - manual: 'By using this option the user can check if any of the given files would be ignored, no matter if they are already present in the work tree or not. This option can only be used together with --dry-run.' - } - ] - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - }, - { - // 'git commit' command - item: 'command', - name: 'git commit', - description: 'Record changes to the repository', - manual: 'Stores the current contents of the index in a new commit along with a log message from the user describing the changes.' + - '
The content to be added can be specified in several ways:' + - '
1. by using git add to incrementally "add" changes to the index before using the commit command (Note: even modified files must be "added");' + - '
2. by using git rm to remove files from the working tree and the index, again before using the commit command;' + - '
3. by listing files as arguments to the commit command, in which case the commit will ignore changes staged in the index, and instead record the current content of the listed files (which must already be known to git);' + - '
4. by using the -a switch with the commit command to automatically "add" changes from all known files (i.e. all files that are already listed in the index) and to automatically "rm" files in the index that have been removed from the working tree, and then perform the actual commit;' + - '
5. by using the --interactive switch with the commit command to decide one by one which files should be part of the commit, before finalizing the operation. Currently, this is done by invoking git add --interactive.' + - '
The --dry-run option can be used to obtain a summary of what is included by any of the above for the next commit by giving the same set of parameters (options and paths).' + - '
If you make a commit and then find a mistake immediately after that, you can recover from it with git reset.', - params: [ - { - name: 'file', - short: 'F', - type: { name: 'array', subtype: 'existingFile' }, - description: 'Files to commit', - manual: 'When files are given on the command line, the command commits the contents of the named files, without recording the changes already staged. The contents of these files are also staged for the next commit on top of what have been staged before.' - }, - { - group: 'Common Options', - params: [ - { - name: 'all', - short: 'a', - type: 'boolean', - description: 'All (unignored) files', - manual: 'Tell the command to automatically stage files that have been modified and deleted, but new files you have not told git about are not affected.' - }, - { - name: 'message', - short: 'm', - type: 'string', - description: 'Commit message', - manual: 'Use the given message as the commit message.' - }, - { - name: 'signoff', - short: 's', - type: 'string', - description: 'Signed off by', - manual: 'Add Signed-off-by line by the committer at the end of the commit log message.' - } - ] - }, - { - group: 'Advanced Options', - params: [ - { - name: 'author', - type: 'string', - description: 'Override the author', - manual: 'Specify an explicit author using the standard A U Thor format. Otherwise is assumed to be a pattern and is used to search for an existing commit by that author (i.e. rev-list --all -i --author=); the commit author is then copied from the first such commit found.' - }, - { - name: 'date', - type: 'string', // Make this of date type - description: 'Override the date', - manual: 'Override the author date used in the commit.' - }, - { - name: 'amend', - type: 'boolean', - description: 'Amend tip', - manual: 'Used to amend the tip of the current branch. Prepare the tree object you would want to replace the latest commit as usual (this includes the usual -i/-o and explicit paths), and the commit log editor is seeded with the commit message from the tip of the current branch. The commit you create replaces the current tip -- if it was a merge, it will have the parents of the current tip as parents -- so the current top commit is discarded.' - }, - { - name: 'verbose', - short: 'v', - type: 'boolean', - description: 'Verbose', - manual: 'Show unified diff between the HEAD commit and what would be committed at the bottom of the commit message template. Note that this diff output doesn\'t have its lines prefixed with #.' - }, - { - name: 'quiet', - short: 'q', - type: 'boolean', - description: 'Quiet', - manual: 'Suppress commit summary message.' - }, - { - name: 'dry-run', - type: 'boolean', - description: 'Dry run', - manual: 'Do not create a commit, but show a list of paths that are to be committed, paths with local changes that will be left uncommitted and paths that are untracked.' - }, - { - name: 'untracked-files', - short: 'u', - type: { - name: 'selection', - data: [ 'no', 'normal', 'all' ] - }, - description: 'Show untracked files', - manual: 'The mode parameter is optional, and is used to specify the handling of untracked files. The possible options are: no - Show no untracked files.
normal Shows untracked files and directories
all Also shows individual files in untracked directories.' - } - ] - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - }, - { - item: 'command', - name: 'git remote', - description: 'Manage set of tracked repositories', - manual: 'Manage the set of repositories ("remotes") whose branches you track.' - }, - { - item: 'command', - name: 'git remote rename', - description: 'Rename a remote', - params: [ - { - name: 'old', - type: 'string' - }, - { - name: 'new', - type: 'string' - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - }, - { - item: 'command', - name: 'git remote add', - description: 'Add a remote', - params: [ - { - name: 'name', - type: 'string', - description: 'A short name for a remote repository' - }, - { - name: 'url', - type: 'string', - description: 'URL that declares where the remote repository is' - } - ], - exec: function(args, context) { - return 'This is only a demo of UI generation.'; - } - } -]; diff --git a/lib/gcli/commands/demo/hg.js b/lib/gcli/commands/demo/hg.js deleted file mode 100644 index 2a39a8d9..00000000 --- a/lib/gcli/commands/demo/hg.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -var host = require('../../util/host'); - -exports.items = [ - { - // Top level 'hg' command - item: 'command', - name: 'hg', - description: 'Mercurial is a free, distributed source control management tool', - manual: 'Mercurial is a free, distributed source control management tool. It efficiently handles projects of any size and offers an easy and intuitive interface.' - }, - { - // Convert a list of patches to a DOM view - item: 'converter', - from: 'patches', - to: 'view', - exec: function(patches, context) { - return { - html: - '\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
${patch.name}
${patch.comment}
\n' + - ' goto\n' + - '
\n', - data: { - patches: patches, - onclick: context.update, - ondblclick: context.updateExec - } - }; - } - }, - { - // 'hg qseries' command - name: 'hg qseries', - description: 'Print the entire series file', - params: [ ], - returnType: 'patches', - exec: function(args, context) { - var spawnSpec = { - cmd: '/usr/local/bin/hg', - args: [ 'qseries' ], - cwd: context.shell.cwd, - env: context.shell.env - }; - - return host.spawn(context, spawnSpec).then(function(output) { - return output.split('\n').map(function(line) { - return { - name: line.split(':', 1)[0], - comment: line.substring(name.length + 2) - }; - }); - }); - } - } -]; diff --git a/lib/gcli/commands/demo/theme.js b/lib/gcli/commands/demo/theme.js deleted file mode 100644 index 46b159a8..00000000 --- a/lib/gcli/commands/demo/theme.js +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -exports.items = [ - { - item: 'type', - name: 'theme', - parent: 'selection', - data: [ 'dark', 'light' ] - }, - { - item: 'command', - name: 'theme', - description: 'Change themes', - params: [ - { - name: 'theme', - type: 'theme', - description: 'The theme to use' - }, - { - name: 'show', - type: 'boolean', - description: 'Display a preview of the current theme', - hidden: true, - option: true - } - ], - exec: function(args, context) { - if (args.show) { - return context.typedData('theme-preview', args); - } - else { - return context.typedData('theme-change', args); - } - } - }, - { - item: 'converter', - from: 'theme-change', - to: 'view', - exec: function(args, context) { - var body = context.document.body; - - // Remove existing themes. This is very dependent on how themes are - // setup. This code will probably require local customization - exports.items[0].data.forEach(function(theme) { - body.classList.remove(theme); - }); - body.classList.add(args.theme); - - return { - html: '
Set theme to ${theme}
', - data: args, - options: { allowEval: true, stack: 'theme.html#change' } - }; - } - }, - { - item: 'converter', - from: 'theme-preview', - to: 'view', - exec: function(args, context) { - return { - html: - '
\n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - ' \n' + - '
${className}Lorem ipsum dolor sit amet ↑ → ↓ ← ██████████
\n' + - '
', - data: args, - options: { allowEval: true, stack: 'theme.html#preview' } - }; - } - } -]; From 75c9db4f90cabc94ced1e99e8687cd7c722e528c Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 11:21:26 +0000 Subject: [PATCH 22/42] runat-1128988: Rationalize commands 1: Create item groups The various places in GCLI where we created a new system all had their own list of items in their own groups, that were mostly the same sets over again. So we broke them into the logical groups to make maintenance easier, and importing simpler. Signed-off-by: Joe Walker --- gcli.js | 44 +++--------------- index.html | 7 +-- lib/gcli/connectors/direct.js | 45 +++--------------- lib/gcli/connectors/index.js | 55 +++------------------- lib/gcli/demo.js | 58 ----------------------- lib/gcli/index.js | 88 +++++++++++++---------------------- lib/gcli/items/basic.js | 50 ++++++++++++++++++++ lib/gcli/items/demo.js | 30 ++++++++++++ lib/gcli/items/remote.js | 29 ++++++++++++ lib/gcli/items/server.js | 31 ++++++++++++ lib/gcli/items/standard.js | 40 ++++++++++++++++ lib/gcli/items/ui.js | 36 ++++++++++++++ 12 files changed, 272 insertions(+), 241 deletions(-) delete mode 100644 lib/gcli/demo.js create mode 100644 lib/gcli/items/basic.js create mode 100644 lib/gcli/items/demo.js create mode 100644 lib/gcli/items/remote.js create mode 100644 lib/gcli/items/server.js create mode 100644 lib/gcli/items/standard.js create mode 100644 lib/gcli/items/ui.js diff --git a/gcli.js b/gcli.js index 79d9ce00..911d052a 100644 --- a/gcli.js +++ b/gcli.js @@ -21,46 +21,14 @@ exports.gcliHome = __dirname; var system = require('./lib/gcli/system').createSystem(); -/* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI - */ var items = [ - require('./lib/gcli/index').items, - - require('./lib/gcli/cli').items, - require('./lib/gcli/commands/clear').items, - require('./lib/gcli/commands/connect').items, - require('./lib/gcli/commands/context').items, - require('./lib/gcli/commands/exec').items, - require('./lib/gcli/commands/global').items, - require('./lib/gcli/commands/help').items, - require('./lib/gcli/commands/intro').items, - require('./lib/gcli/commands/lang').items, - require('./lib/gcli/commands/mocks').items, - require('./lib/gcli/commands/pref').items, - require('./lib/gcli/commands/preflist').items, - require('./lib/gcli/commands/test').items, - - require('./lib/gcli/commands/demo/alert').items, - require('./lib/gcli/commands/demo/demo').items, - require('./lib/gcli/commands/demo/echo').items, - require('./lib/gcli/commands/demo/sleep').items, + require('./lib/gcli/items/basic').items, + require('./lib/gcli/items/ui').items, + require('./lib/gcli/items/remote').items, + require('./lib/gcli/items/standard').items, + require('./lib/gcli/items/demo').items, + require('./lib/gcli/items/server').items, - require('./lib/gcli/commands/server/exit').items, - require('./lib/gcli/commands/server/firefox').items, - require('./lib/gcli/commands/server/orion').items, - require('./lib/gcli/commands/server/server').items, - require('./lib/gcli/commands/server/standard').items ].reduce(function(prev, curr) { return prev.concat(curr); }, []); system.addItems(items); diff --git a/index.html b/index.html index ed7eea48..9ca4f09e 100644 --- a/index.html +++ b/index.html @@ -25,12 +25,13 @@ paths: { i18n: '../scripts/i18n', text: '../scripts/text' } }); - var modules = [ 'gcli/index', 'gcli/demo', 'gcli/test/index' ]; + var modules = [ 'gcli/index', 'gcli/items/demo', 'gcli/test/index' ]; require(modules, function(gcli, demo, test) { // Add the commands/types/converters as required var system = gcli.createSystem(); - system.addItems(gcli.items); // The basic set that most people need - system.addItems(demo.items); // Demo commands + system.addItems(gcli.items); // Common infrastructure: types, etc + system.addItems(gcli.commandItems); // Common set of useful commands + system.addItems(demo.items); // Extra demo commands gcli.createTerminal(system).then(function(terminal) { terminal.language.showIntro(); // Intro text diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index edb784b1..e78b11ca 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -25,44 +25,6 @@ var Promise = require('../util/promise').Promise; var Remoter = require('./remoted').Remoter; var Connection = require('./connectors').Connection; -var items = [ - require('../types/delegate').items, - require('../types/selection').items, - - require('../types/array').items, - require('../types/boolean').items, - require('../types/command').items, - require('../types/date').items, - require('../types/file').items, - require('../types/javascript').items, - require('../types/node').items, - require('../types/number').items, - require('../types/resource').items, - require('../types/setting').items, - require('../types/string').items, - require('../types/union').items, - require('../types/url').items, - - require('../cli').items, - - require('../commands/clear').items, - require('../commands/connect').items, - require('../commands/context').items, - require('../commands/exec').items, - require('../commands/global').items, - require('../commands/help').items, - require('../commands/intro').items, - require('../commands/lang').items, - require('../commands/mocks').items, - require('../commands/preflist').items, - require('../commands/pref').items, - require('../commands/test').items, - require('../commands/demo/alert').items, - require('../commands/demo/echo').items, - require('../commands/demo/sleep').items - -].reduce(function(prev, curr) { return prev.concat(curr); }, []); - exports.items = [ { // Communicate with a 'remote' server that isn't remote at all @@ -78,6 +40,13 @@ exports.items = [ function DirectConnection() { this._emit = this._emit.bind(this); + // The items to use in our new command line + var items = [ + require('../items/basic').items, + require('../items/standard').items, + require('../items/demo').items, + ].reduce(function(prev, curr) { return prev.concat(curr); }, []); + // This is the 'server' var system = createSystem(); system.addItems(items); diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index e375a661..a9ee6ab8 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -23,58 +23,15 @@ var Types = require('../types/types').Types; // Patch-up IE9 require('../util/legacy'); -/* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - lib/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI +/** + * */ var items = [ - // First we need to add the local types which other types depend on - require('../types/delegate').items, - require('../types/selection').items, - require('../types/array').items, - - require('../types/boolean').items, - require('../types/command').items, - require('../types/date').items, - require('../types/file').items, - require('../types/javascript').items, - require('../types/node').items, - require('../types/number').items, - require('../types/resource').items, - require('../types/setting').items, - require('../types/string').items, - require('../types/union').items, - require('../types/url').items, - - require('../fields/fields').items, - require('../fields/delegate').items, - require('../fields/selection').items, - - require('../ui/intro').items, - require('../ui/focus').items, - - require('../converters/converters').items, - require('../converters/basic').items, - require('../converters/html').items, - require('../converters/terminal').items, - - require('../languages/command').items, - require('../languages/javascript').items, - - require('./direct').items, - // require('./rdp').items, // Firefox remote debug protocol - require('./websocket').items, - require('./xhr').items, + require('../items/basic').items, + require('../items/ui').items, + require('../items/remote').items, + // TODO: why is this here? require('../commands/context').items, ].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/demo.js b/lib/gcli/demo.js deleted file mode 100644 index 62a8e230..00000000 --- a/lib/gcli/demo.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -/* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI - */ -exports.items = [ - require('./cli').items, - require('./commands/clear').items, - require('./commands/connect').items, - require('./commands/context').items, - require('./commands/exec').items, - require('./commands/global').items, - require('./commands/help').items, - require('./commands/intro').items, - require('./commands/lang').items, - require('./commands/mocks').items, - require('./commands/pref').items, - require('./commands/preflist').items, - require('./commands/test').items, - - require('./commands/demo/alert').items, - require('./commands/demo/bugs').items, - require('./commands/demo/demo').items, - require('./commands/demo/echo').items, - require('./commands/demo/edit').items, - // require('./commands/demo/git').items, - // require('./commands/demo/hg').items, - require('./commands/demo/sleep').items, - require('./commands/demo/theme').items, - - // Exclude Node commands on web -].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/index.js b/lib/gcli/index.js index e630ac7d..57e4e7d6 100644 --- a/lib/gcli/index.js +++ b/lib/gcli/index.js @@ -16,66 +16,44 @@ 'use strict'; -var createSystem = require('./system').createSystem; -var Terminal = require('./ui/terminal').Terminal; - -// Patch-up old browsers -require('./util/legacy'); - /* - * GCLI is built from a number of components (called items) composed as - * required for each environment. - * When adding to or removing from this list, we should keep the basics in sync - * with the other environments. - * See: - * - lib/gcli/index.js: Generic basic set (without commands) - * - lib/gcli/demo.js: Adds demo commands to basic set for use in web demo - * - gcli.js: Add commands to basic set for use in Node command line - * - mozilla/gcli/index.js: From scratch listing for Firefox - * - lib/gcli/connectors/index.js: Client only items when executing remotely - * - lib/gcli/connectors/direct.js: Test items for connecting to in-process GCLI + * The intent of this module is to pull together all the parts needed to start + * a command line to make it easier to get started. + * + * Basic usage is like this: + * + * var gcli = require('gcli/index'); + * + * // A system is a set of commands/types/etc that the command line uses + * var system = gcli.createSystem(); + * system.addItems(gcli.items); + * system.addItems(gcli.commandItems); + * system.addItems([ + * // Your own commands go here + * ]); + * + * // Create the UI + * gcli.createTerminal(system).then(function(terminal) { + * // Take any actions when the command line starts for example + * terminal.language.showIntro(); + * }); */ -exports.items = [ - require('./types/delegate').items, - require('./types/selection').items, - require('./types/array').items, - - require('./types/boolean').items, - require('./types/command').items, - require('./types/date').items, - require('./types/file').items, - require('./types/javascript').items, - require('./types/node').items, - require('./types/number').items, - require('./types/resource').items, - require('./types/setting').items, - require('./types/string').items, - require('./types/union').items, - require('./types/url').items, - require('./fields/fields').items, - require('./fields/delegate').items, - require('./fields/selection').items, - - require('./ui/focus').items, - require('./ui/intro').items, - - require('./converters/converters').items, - require('./converters/basic').items, - require('./converters/html').items, - require('./converters/terminal').items, +// Patch-up old browsers +require('./util/legacy'); - require('./languages/command').items, - require('./languages/javascript').items, +exports.createSystem = require('./system').createSystem; - // require('./connectors/direct').items, // Loopback for testing only - // require('./connectors/rdp').items, // Firefox remote debug protocol - require('./connectors/websocket').items, - require('./connectors/xhr').items, +var Terminal = require('./ui/terminal').Terminal; +exports.createTerminal = Terminal.create.bind(Terminal); - // No commands in the basic set +/** + * This is all the items we need for a basic GCLI (except for the commands) + */ +exports.items = [ + require('./items/basic').items, + require('./items/ui').items, + require('./items/remote').items, ].reduce(function(prev, curr) { return prev.concat(curr); }, []); -exports.createSystem = createSystem; - -exports.createTerminal = Terminal.create.bind(Terminal); +exports.commandItems = require('./items/standard').items; diff --git a/lib/gcli/items/basic.js b/lib/gcli/items/basic.js new file mode 100644 index 00000000..6bb660ea --- /dev/null +++ b/lib/gcli/items/basic.js @@ -0,0 +1,50 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * This is a list of the types and converters that will be needed in almost all + * command lines. The types are listed in 2 sets because delegate and selection + * are depended on by others, (e.g boolean depends on selection) and array is + * built into cli.js parsing somewhat. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../types/delegate').items, + require('../types/selection').items, + require('../types/array').items, + + require('../types/boolean').items, + require('../types/command').items, + require('../types/date').items, + require('../types/file').items, + require('../types/javascript').items, + require('../types/node').items, + require('../types/number').items, + require('../types/resource').items, + require('../types/setting').items, + require('../types/string').items, + require('../types/union').items, + require('../types/url').items, + + require('../converters/converters').items, + require('../converters/basic').items, + require('../converters/html').items, + require('../converters/terminal').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/demo.js b/lib/gcli/items/demo.js new file mode 100644 index 00000000..88d6f390 --- /dev/null +++ b/lib/gcli/items/demo.js @@ -0,0 +1,30 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * This is a list of demo commands. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../commands/demo/alert').items, + require('../commands/demo/demo').items, + require('../commands/demo/echo').items, + require('../commands/demo/sleep').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/remote.js b/lib/gcli/items/remote.js new file mode 100644 index 00000000..402ae545 --- /dev/null +++ b/lib/gcli/items/remote.js @@ -0,0 +1,29 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * This is a list of the available connectors when linking to GCLIs together + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../connectors/websocket').items, + require('../connectors/xhr').items, + require('../connectors/direct').items, // Generally for testing only + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/server.js b/lib/gcli/items/server.js new file mode 100644 index 00000000..1697e89b --- /dev/null +++ b/lib/gcli/items/server.js @@ -0,0 +1,31 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * This is a list of the commands designed to run in nodejs / io.js. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../commands/server/exit').items, + require('../commands/server/firefox').items, + require('../commands/server/orion').items, + require('../commands/server/server').items, + require('../commands/server/standard').items + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/standard.js b/lib/gcli/items/standard.js new file mode 100644 index 00000000..1781379a --- /dev/null +++ b/lib/gcli/items/standard.js @@ -0,0 +1,40 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * This is a list of the standard commands that are likely to be in most + * command lines. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../cli').items, + require('../commands/clear').items, + require('../commands/connect').items, + require('../commands/context').items, + require('../commands/exec').items, + require('../commands/global').items, + require('../commands/help').items, + require('../commands/intro').items, + require('../commands/lang').items, + require('../commands/mocks').items, + require('../commands/pref').items, + require('../commands/preflist').items, + require('../commands/test').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); diff --git a/lib/gcli/items/ui.js b/lib/gcli/items/ui.js new file mode 100644 index 00000000..374364b2 --- /dev/null +++ b/lib/gcli/items/ui.js @@ -0,0 +1,36 @@ +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +/** + * This is a list of the fields, settings and languages that we need to build + * a user interface. + * + * Keeping this module small helps reduce bringing in unwanted dependencies. + */ +exports.items = [ + require('../fields/fields').items, + require('../fields/delegate').items, + require('../fields/selection').items, + + require('../ui/focus').items, + require('../ui/intro').items, + + require('../languages/command').items, + require('../languages/javascript').items, + +].reduce(function(prev, curr) { return prev.concat(curr); }, []); From 397538072039b730e0fbdc662950af71d07c6998 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 11:22:44 +0000 Subject: [PATCH 23/42] runat-1128988: Add system.toString for debugging purposes. Signed-off-by: Joe Walker --- lib/gcli/converters/converters.js | 9 +++++++++ lib/gcli/fields/fields.js | 7 +++++++ lib/gcli/system.js | 10 ++++++++++ 3 files changed, 26 insertions(+) diff --git a/lib/gcli/converters/converters.js b/lib/gcli/converters/converters.js index 38b2bdc6..545755a4 100644 --- a/lib/gcli/converters/converters.js +++ b/lib/gcli/converters/converters.js @@ -208,6 +208,15 @@ Converters.prototype.get = function(from, to) { return converter; }; +/** + * Get all the registered converters. Most for debugging + */ +Converters.prototype.getAll = function() { + return Object.keys(this._registered.from).map(function(name) { + return this._registered.from[name]; + }.bind(this)); +}; + /** * Helper for get to pick the best fallback converter */ diff --git a/lib/gcli/fields/fields.js b/lib/gcli/fields/fields.js index d8897b27..c9718473 100644 --- a/lib/gcli/fields/fields.js +++ b/lib/gcli/fields/fields.js @@ -199,6 +199,13 @@ Fields.prototype.get = function(type, options) { return new FieldConstructor(type, options); }; +/** + * Get all the registered fields. Most for debugging + */ +Fields.prototype.getAll = function() { + return this._fieldCtors.slice(); +}; + exports.Fields = Fields; /** diff --git a/lib/gcli/system.js b/lib/gcli/system.js index d1da29b3..2fae1735 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -174,6 +174,16 @@ exports.createSystem = function(options) { pendingChanges = false; return Promise.all(promises); + }, + + toString: function() { + return 'System [' + + 'commands:' + components.command.getAll().length + ', ' + + 'connectors:' + components.connector.getAll().length + ', ' + + 'converters:' + components.converter.getAll().length + ', ' + + 'fields:' + components.field.getAll().length + ', ' + + 'settings:' + components.setting.getAll().length + ', ' + + 'types:' + components.type.getTypeNames().length + ']'; } }; From dc4a419d3ab7e156ecd78d3eaada7e111d0b9510 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 11:23:14 +0000 Subject: [PATCH 24/42] runat-1128988: Improve 'command not found' error message Signed-off-by: Joe Walker --- lib/gcli/cli.js | 3 ++- lib/gcli/nls/strings.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/gcli/cli.js b/lib/gcli/cli.js index 2e6e15b4..0fdaee9f 100644 --- a/lib/gcli/cli.js +++ b/lib/gcli/cli.js @@ -694,7 +694,8 @@ Object.defineProperty(Requisition.prototype, 'status', { */ Requisition.prototype.getStatusMessage = function() { if (this.commandAssignment.getStatus() !== Status.VALID) { - return l10n.lookup('cliUnknownCommand'); + return l10n.lookupFormat('cliUnknownCommand2', + [ this.commandAssignment.arg.text ]); } var assignments = this.getAssignments(); diff --git a/lib/gcli/nls/strings.js b/lib/gcli/nls/strings.js index 96b7c1b7..c9b7d785 100644 --- a/lib/gcli/nls/strings.js +++ b/lib/gcli/nls/strings.js @@ -38,7 +38,7 @@ var i18n = { cliOptions: 'Available Options', // The error message when the user types a command that isn't registered - cliUnknownCommand: 'Invalid Command', + cliUnknownCommand2: 'Invalid Command: \'%1$S\'.', // A parameter should have a value, but doesn't cliIncompleteParam: 'Value required for \'%1$S\'.', From dba90086ccb0df32c49589e95d1218758dd29956 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 13:58:26 +0000 Subject: [PATCH 25/42] runat-1128988: Improve 'type not found' debugging Signed-off-by: Joe Walker --- lib/gcli/system.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/gcli/system.js b/lib/gcli/system.js index 2fae1735..55a9cb7d 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -56,7 +56,13 @@ exports.createSystem = function(options) { }; var addItem = function(item) { - components[getItemType(item)].add(item); + try { + components[getItemType(item)].add(item); + } + catch (ex) { + console.error('While adding: ' + item.name); + throw ex; + } }; var removeItem = function(item) { From 5c4c5f7b13a000973d39e6c55a3d1a9125340929 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 14:00:07 +0000 Subject: [PATCH 26/42] runat-1128988: Add 'location' support to filter commands Adding a 'location' to a system makes it ignore commands that don't have a matching runAt property. This is principly for client/server setups where we import commands from the server to the client, so a system with `{ location: 'client' }` will silently ignore commands with `{ runAt: 'server' }`. Any system without a location will accept commands with any runAt property (including none). Signed-off-by: Joe Walker --- lib/gcli/commands/commands.js | 15 +++++++++++++-- lib/gcli/system.js | 13 +++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/gcli/commands/commands.js b/lib/gcli/commands/commands.js index a1f073e8..0cc4cfbb 100644 --- a/lib/gcli/commands/commands.js +++ b/lib/gcli/commands/commands.js @@ -327,9 +327,14 @@ exports.Parameter = Parameter; /** * A store for a list of commands + * @param types Each command uses a set of Types to parse its parameters so the + * Commands container needs access to the list of available types. + * @param location String that, if set will force all commands to have a + * matching runAt property to be accepted */ -function Commands(types) { +function Commands(types, location) { this.types = types; + this.location = location; // A lookup hash of our registered commands this._commands = {}; @@ -345,9 +350,15 @@ function Commands(types) { /** * Add a command to the list of known commands. * @param commandSpec The command and its metadata. - * @return The new command + * @return The new command, or null if a location property has been set and the + * commandSpec doesn't have a matching runAt property. */ Commands.prototype.add = function(commandSpec) { + if (this.location != null && commandSpec.runAt != null && + commandSpec.runAt !== this.location) { + return; + } + if (this._commands[commandSpec.name] != null) { // Roughly commands.remove() without the event call, which we do later delete this._commands[commandSpec.name]; diff --git a/lib/gcli/system.js b/lib/gcli/system.js index 55a9cb7d..f0da194c 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -26,7 +26,16 @@ var Settings = require('./settings').Settings; var Types = require('./types/types').Types; /** - * This is the heart of the API that we expose to the outside + * This is the heart of the API that we expose to the outside. + * @param options Object that customizes how the system acts. Valid properties: + * - commands, connectors, converters, fields, languages, settings, types: + * Custom configured manager objects for these item types + * - location: a system with a location will ignore commands that don't have a + * matching runAt property. This is principly for client/server setups where + * we import commands from the server to the client, so a system with + * `{ location: 'client' }` will silently ignore commands with + * `{ runAt: 'server' }`. Any system without a location will accept commands + * with any runAt property (including none). */ exports.createSystem = function(options) { options = options || {}; @@ -43,7 +52,7 @@ exports.createSystem = function(options) { type: options.types || new Types() }; components.setting = new Settings(components.type); - components.command = new Commands(components.type); + components.command = new Commands(components.type, location); var getItemType = function(item) { if (item.item) { From 7a95a196ab10fa29adfa8b216c1293b56365536c Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 6 Feb 2015 14:47:01 +0000 Subject: [PATCH 27/42] runat-1128988: Fix lingering input bug When an async command was run, the input element didn't get cleared until the async command had completed. This clears it as soon as return is pressed. Signed-off-by: Joe Walker --- lib/gcli/languages/command.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/gcli/languages/command.js b/lib/gcli/languages/command.js index bc9f4381..0a950c8e 100644 --- a/lib/gcli/languages/command.js +++ b/lib/gcli/languages/command.js @@ -288,6 +288,9 @@ var commandLanguage = exports.commandLanguage = { this.terminal.history.add(input); this.terminal.unsetChoice().then(null, util.errorHandler); + this.terminal.inputElement.value = ''; + this.terminal_previousValue = this.terminal.inputElement.value; + return this.requisition.exec().then(function() { this.textChanged(); return true; From 9ca5302c7291128a34e14970072eb104343568d0 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 10:57:44 +0000 Subject: [PATCH 28/42] runat-1128988: Minor comment clarifications and add a missing ';' Signed-off-by: Joe Walker --- lib/gcli/connectors/connectors.js | 2 +- lib/gcli/connectors/index.js | 8 ++++++-- lib/gcli/types/types.js | 4 ++-- lib/gcli/util/host.js | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/gcli/connectors/connectors.js b/lib/gcli/connectors/connectors.js index edc24e86..1578f9c5 100644 --- a/lib/gcli/connectors/connectors.js +++ b/lib/gcli/connectors/connectors.js @@ -149,7 +149,7 @@ Connectors.prototype.get = function(name) { if (name == null) { name = (defaultConnectorName == null) ? Object.keys(this._registered)[0] : - defaultConnectorName + defaultConnectorName; } defaultConnectorName = name; diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index a9ee6ab8..7042c697 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -38,8 +38,8 @@ var items = [ /** * These are the commands stored on the remote side that have converters which - * we'll need to present the data. Ideally connection.specs would transfer - * these, that doesn't happen yet so we add them manually + * we'll need to present the data. Ideally front.specs() would transfer these, + * that doesn't happen yet so we add them manually */ var requiredConverters = [ require('../cli').items, @@ -132,6 +132,10 @@ exports.addLocalFunctions = function(specs, connection) { return specs; }; +/** + * Go through all the commands removing any that are associated with the given + * front. The method of association is the hack in addLocalFunctions. + */ exports.removeRemoteItems = function(system, connection) { system.commands.getAll().forEach(function(command) { if (command.connection === connection) { diff --git a/lib/gcli/types/types.js b/lib/gcli/types/types.js index e92d2626..5b672c71 100644 --- a/lib/gcli/types/types.js +++ b/lib/gcli/types/types.js @@ -946,8 +946,8 @@ function Type() { } /** - * Get a JSONable data structure that entirely describes this type - * @param commandName/paramName The names of the command and parameter which we + * Get a JSONable data structure that entirely describes this type. + * commandName and paramName are the names of the command and parameter which we * are remoting to help the server get back to the remoted action. */ Type.prototype.getSpec = function(commandName, paramName) { diff --git a/lib/gcli/util/host.js b/lib/gcli/util/host.js index 9e84437d..0012a4fd 100644 --- a/lib/gcli/util/host.js +++ b/lib/gcli/util/host.js @@ -75,7 +75,7 @@ exports.Highlighter = Highlighter; /** * Helper to execute an arbitrary OS-level command. * @param context From which we get the shell containing cwd and env - * @param execSpec Object containing some of the following properties: + * @param spawnSpec Object containing some of the following properties: * - cmd (string): The command to execute (required) * - args (string[]): The arguments to pass to the command (default: []) * - cwd (string): The current working directory From 1a8fd03143a1bdbd7ca8c3fc954e2e949a510505 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 11:01:02 +0000 Subject: [PATCH 29/42] runat-1128988: Introduce GcliFront to mirror protocol.js Front I think GCLI should probably use protocol.js, and using a similar interface is a step in the right direction. Also it simplifies calling remote interfaces. Signed-off-by: Joe Walker --- lib/gcli/commands/connect.js | 40 ++++++------ lib/gcli/connectors/index.js | 42 ++++++------- lib/gcli/connectors/remoted.js | 108 +++++++++++++++++++++++++++++++-- lib/gcli/test/testRemoteWs.js | 12 ++-- lib/gcli/test/testRemoteXhr.js | 12 ++-- lib/gcli/types/delegate.js | 9 +-- lib/gcli/types/selection.js | 12 +--- web/gcli/types/fileparser.js | 18 +++--- web/gcli/util/host.js | 15 +++-- 9 files changed, 172 insertions(+), 96 deletions(-) diff --git a/lib/gcli/commands/connect.js b/lib/gcli/commands/connect.js index bc7d7184..beb24083 100644 --- a/lib/gcli/commands/connect.js +++ b/lib/gcli/commands/connect.js @@ -18,11 +18,12 @@ var l10n = require('../util/l10n'); var cli = require('../cli'); +var GcliFront = require('../connectors/remoted').GcliFront; /** * A lookup of the current connection */ -var connections = {}; +var fronts = {}; /** * 'connection' type @@ -32,8 +33,8 @@ var connection = { name: 'connection', parent: 'selection', lookup: function() { - return Object.keys(connections).map(function(prefix) { - return { name: prefix, value: connections[prefix] }; + return Object.keys(fronts).map(function(prefix) { + return { name: prefix, value: fronts[prefix] }; }); } }; @@ -87,19 +88,19 @@ var connect = { returnType: 'string', exec: function(args, context) { - if (connections[args.prefix] != null) { + if (fronts[args.prefix] != null) { throw new Error(l10n.lookupFormat('connectDupReply', [ args.prefix ])); } - var connector = args.method || context.system.connectors.get('xhr'); + args.method = args.method || context.system.connectors.get('xhr'); - return connector.connect(args.url).then(function(connection) { - // Nasty: stash the prefix on the connection to help us tidy up - connection.prefix = args.prefix; - connections[args.prefix] = connection; + return GcliFront.create(args.method, args.url).then(function(front) { + // Nasty: stash the prefix on the front to help us tidy up + front.prefix = args.prefix; + fronts[args.prefix] = front; - return connection.call('specs').then(function(specs) { - var remoter = this.createRemoter(args.prefix, connection); + return front.specs().then(function(specs) { + var remoter = this.createRemoter(args.prefix, front); var commands = cli.getMapping(context).requisition.system.commands; commands.addProxyCommands(specs, remoter, args.prefix, args.url); @@ -116,7 +117,7 @@ var connect = { * When we register a set of remote commands, we need to provide a proxy * executor. This is that executor. */ - createRemoter: function(prefix, connection) { + createRemoter: function(prefix, front) { return function(cmdArgs, context) { var typed = context.typed; @@ -126,12 +127,7 @@ var connect = { typed = typed.substring(prefix.length).replace(/^ */, ''); } - var data = { - typed: typed, - args: cmdArgs - }; - - return connection.call('execute', data).then(function(reply) { + return front.execute(typed).then(function(reply) { var typedData = context.typedData(reply.type, reply.data); if (!reply.error) { return typedData; @@ -162,11 +158,11 @@ var disconnect = { returnType: 'string', exec: function(args, context) { - var connection = args.prefix; - return connection.disconnect().then(function() { + var front = args.prefix; + return front.connection.disconnect().then(function() { var commands = cli.getMapping(context).requisition.system.commands; - var removed = commands.removeProxyCommands(connection.prefix); - delete connections[connection.prefix]; + var removed = commands.removeProxyCommands(front.prefix); + delete fronts[front.prefix]; return l10n.lookupFormat('disconnectReply', [ removed.length ]); }); } diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 7042c697..83be7a4a 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -17,8 +17,7 @@ 'use strict'; var createSystem = require('../system').createSystem; -var Commands = require('../commands/commands').Commands; -var Types = require('../types/types').Types; +var GcliFront = require('../connectors/remoted').GcliFront; // Patch-up IE9 require('../util/legacy'); @@ -69,42 +68,41 @@ exports.connect = function(options) { system.addItems(items); system.addItems(requiredConverters); - var connector = system.connectors.get(options.connector); - return connector.connect(options.url).then(function(connection) { - system.connection = connection; - connection.on('commandsChanged', function(specs) { - exports.addItems(system, specs, connection); + var connector = system.connectors.get(options.method); + GcliFront.create(connector, options.url).then(function(front) { + front.on('commandsChanged', function(specs) { + exports.addItems(system, specs, front); }); - return connection.call('specs').then(function(specs) { - exports.addItems(system, specs, connection); + return front.specs().then(function(specs) { + exports.addItems(system, specs, front); return system; }); - }); + }.bind(this)); }; -exports.addItems = function(system, specs, connection) { - exports.removeRemoteItems(system, connection); - var remoteItems = exports.addLocalFunctions(specs, connection); +exports.addItems = function(system, specs, front) { + exports.removeRemoteItems(system, front); + var remoteItems = exports.addLocalFunctions(specs, front); system.addItems(remoteItems); }; /** * Take the data from the 'specs' command (or the 'commandsChanged' event) and - * add function to proxy the execution back over the connection + * add function to proxy the execution back over the front */ -exports.addLocalFunctions = function(specs, connection) { - // Inject an 'exec' function into the commands, and the connection into +exports.addLocalFunctions = function(specs, front) { + // Inject an 'exec' function into the commands, and the front into // all the remote types specs.forEach(function(commandSpec) { - // HACK: Tack the connection to the command so we know how to remove it + // HACK: Tack the front to the command so we know how to remove it // in removeRemoteItems() below - commandSpec.connection = connection; + commandSpec.front = front; // TODO: removeRemoteItems() doesn't remove types, so do we need this? commandSpec.params.forEach(function(param) { if (typeof param.type !== 'string') { - param.type.connection = connection; + param.type.front = front; } }); @@ -114,7 +112,7 @@ exports.addLocalFunctions = function(specs, connection) { typed: (context.prefix ? context.prefix + ' ' : '') + context.typed }; - return connection.call('execute', data).then(function(reply) { + return front.execute(data).then(function(reply) { var typedData = context.typedData(reply.type, reply.data); if (!reply.error) { return typedData; @@ -136,9 +134,9 @@ exports.addLocalFunctions = function(specs, connection) { * Go through all the commands removing any that are associated with the given * front. The method of association is the hack in addLocalFunctions. */ -exports.removeRemoteItems = function(system, connection) { +exports.removeRemoteItems = function(system, front) { system.commands.getAll().forEach(function(command) { - if (command.connection === connection) { + if (command.front === front) { system.commands.remove(command); } }); diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index a3ba4c2c..4a25f4d7 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -127,7 +127,7 @@ Remoter.prototype.exposed = { * - message: The message to display to the user * - predictions: An array of suggested values for the given parameter */ - typeparse: method(function(typed, param) { + parseType: method(function(typed, param) { return this.requisition.update(typed).then(function() { var assignment = this.requisition.getAssignment(param); @@ -151,7 +151,7 @@ Remoter.prototype.exposed = { * Get the incremented value of some type * @return a promise of a string containing the new argument text */ - typeincrement: method(function(typed, param) { + incrementType: method(function(typed, param) { return this.requisition.update(typed).then(function() { var assignment = this.requisition.getAssignment(param); return this.requisition.increment(assignment).then(function() { @@ -168,9 +168,9 @@ Remoter.prototype.exposed = { }), /** - * See typeincrement + * See incrementType */ - typedecrement: method(function(typed, param) { + decrementType: method(function(typed, param) { return this.requisition.update(typed).then(function() { var assignment = this.requisition.getAssignment(param); return this.requisition.decrement(assignment).then(function() { @@ -189,7 +189,7 @@ Remoter.prototype.exposed = { /** * Perform a lookup on a selection type to get the allowed values */ - selectioninfo: method(function(commandName, paramName, action) { + getSelectionInfo: method(function(commandName, paramName, action) { var command = this.requisition.system.commands.get(commandName); if (command == null) { throw new Error('No command called \'' + commandName + '\''); @@ -251,7 +251,7 @@ Remoter.prototype.exposed = { /** * Examine the filesystem for file matches */ - parsefile: method(function(typed, filetype, existing, matches) { + parseFile: method(function(typed, filetype, existing, matches) { var options = { filetype: filetype, existing: existing, @@ -281,3 +281,99 @@ Remoter.prototype.exposed = { response: RetVal("json") }) }; + + +/** + * Asynchronous construction. Use GcliFront(); + * @private + */ +function GcliFront() { + throw new Error('Use GcliFront.create().then(front => ...)'); +} + +/** + * + */ +GcliFront.create = function(connector, url) { + return connector.connect(url).then(function(connection) { + var front = Object.create(GcliFront.prototype); + return front._init(connection); + }); +}; + +/** + * Asynchronous construction. Use GcliFront(); + * @private + */ +GcliFront.prototype._init = function(connection) { + this.connection = connection; + return this; +}; + +GcliFront.prototype.on = function(eventName, action) { + this.connection.on(eventName, action); +}; + +GcliFront.prototype.off = function(eventName, action) { + this.connection.off(eventName, action); +}; + + +GcliFront.prototype.specs = function() { + var data = { + }; + return this.connection.call('specs', data); +}; + +GcliFront.prototype.execute = function(typed) { + var data = { + typed: typed + }; + return this.connection.call('execute', data); +}; + +GcliFront.prototype.parseFile = function(typed, filetype, existing, matches) { + var data = { + typed: typed, + filetype: filetype, + existing: existing, + matches: matches + }; + return this.connection.call('parseFile', data); +}; + +GcliFront.prototype.parseType = function(typed, param) { + var data = { + typed: typed, + param: param + }; + return this.connection.call('parseType', data); +}; + +GcliFront.prototype.incrementType = function(typed, param) { + var data = { + typed: typed, + param: param + }; + return this.connection.call('incrementType', data); +}; + +GcliFront.prototype.decrementType = function(typed, param) { + var data = { + typed: typed, + param: param + }; + return this.connection.call('decrementType', data); +}; + +GcliFront.prototype.system = function(cmd, args, cwd, env) { + var data = { + cmd: cmd, + args: args, + cwd: cwd, + env: env + }; + return this.connection.call('system', data); +}; + +exports.GcliFront = GcliFront; diff --git a/lib/gcli/test/testRemoteWs.js b/lib/gcli/test/testRemoteWs.js index 2f5f15aa..b67c7240 100644 --- a/lib/gcli/test/testRemoteWs.js +++ b/lib/gcli/test/testRemoteWs.js @@ -59,8 +59,8 @@ exports.testRemoteWebsocket = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -88,8 +88,8 @@ exports.testRemoteWebsocket = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -442,8 +442,8 @@ exports.testRemoteWebsocket = function(options) { unassigned: [ ], args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); }, arg: ' remote', status: 'VALID', diff --git a/lib/gcli/test/testRemoteXhr.js b/lib/gcli/test/testRemoteXhr.js index e0c6359b..1792be19 100644 --- a/lib/gcli/test/testRemoteXhr.js +++ b/lib/gcli/test/testRemoteXhr.js @@ -59,8 +59,8 @@ exports.testRemoteXhr = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -88,8 +88,8 @@ exports.testRemoteXhr = function(options) { check: { args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); } } } @@ -442,8 +442,8 @@ exports.testRemoteXhr = function(options) { unassigned: [ ], args: { prefix: { - value: function(connection) { - assert.is(connection.prefix, 'remote', 'disconnecting remote'); + value: function(front) { + assert.is(front.prefix, 'remote', 'disconnecting remote'); }, arg: ' remote', status: 'VALID', diff --git a/lib/gcli/types/delegate.js b/lib/gcli/types/delegate.js index 3b63eb24..1e9cb99f 100644 --- a/lib/gcli/types/delegate.js +++ b/lib/gcli/types/delegate.js @@ -108,8 +108,7 @@ exports.items = [ }, parse: function(arg, context) { - var args = { typed: context.typed, param: this.param }; - return this.connection.call('typeparse', args).then(function(json) { + return this.front.parseType(context.typed, this.param).then(function(json) { var status = Status.fromString(json.status); var val = { stringified: arg.text }; return new Conversion(val, arg, status, json.message, json.predictions); @@ -117,15 +116,13 @@ exports.items = [ }, decrement: function(value, context) { - var args = { typed: context.typed, param: this.param }; - return this.connection.call('typedecrement', args).then(function(json) { + return this.front.decrementType(context.typed, this.param).then(function(json) { return { stringified: json.arg }; }); }, increment: function(value, context) { - var args = { typed: context.typed, param: this.param }; - return this.connection.call('typeincrement', args).then(function(json) { + return this.front.incrementType(context.typed, this.param).then(function(json) { return { stringified: json.arg }; }); } diff --git a/lib/gcli/types/selection.js b/lib/gcli/types/selection.js index c9b79604..4cd2c92f 100644 --- a/lib/gcli/types/selection.js +++ b/lib/gcli/types/selection.js @@ -126,19 +126,11 @@ SelectionType.prototype.getLookup = function(context) { var reply; if (this.remoteLookup) { - reply = this.connection.call('selectioninfo', { - action: 'lookup', - commandName: this.commandName, - paramName: this.paramName - }); + reply = this.front.getSelectionInfo('lookup', this.commandName, this.paramName); reply = resolve(reply, context); } else if (this.remoteData) { - reply = this.connection.call('selectioninfo', { - action: 'data', - commandName: this.commandName, - paramName: this.paramName - }); + reply = this.front.getSelectionInfo('data', this.commandName, this.paramName); reply = resolve(reply, context).then(this._dataToLookup); } else if (typeof this.lookup === 'function') { diff --git a/web/gcli/types/fileparser.js b/web/gcli/types/fileparser.js index b8e6c2b8..908df045 100644 --- a/web/gcli/types/fileparser.js +++ b/web/gcli/types/fileparser.js @@ -18,6 +18,8 @@ var Promise = require('../util/promise').Promise; var Status = require('./types').Status; +var util = require('../util/util'); +var GcliFront = require('../connectors/remoted').GcliFront; /** * Helper for the parse() function from the file type. @@ -38,23 +40,19 @@ var Status = require('./types').Status; * and can contain a boolean 'complete' property */ exports.parse = function(context, typed, options) { - var data = { - typed: typed, - filetype: options.filetype, - existing: options.existing, - matches: options.matches == null ? undefined : options.matches.source - }; + var matches = options.matches == null ? undefined : options.matches.source; - var connectors = context.system.connectors; - return connectors.get().connect().then(function(connection) { - return connection.call('parsefile', data).then(function(reply) { + var connector = context.system.connectors.get(); + return GcliFront.create(connector).then(function(front) { + return front.parseFile(typed, options.filetype, + options.existing, matches).then(function(reply) { reply.status = Status.fromString(reply.status); if (reply.predictions != null) { reply.predictor = function() { return Promise.resolve(reply.predictions); }; } - connection.disconnect(); + front.connection.disconnect().then(null, util.errorHandler); return reply; }); }); diff --git a/web/gcli/util/host.js b/web/gcli/util/host.js index fef711d8..f535ebb4 100644 --- a/web/gcli/util/host.js +++ b/web/gcli/util/host.js @@ -19,6 +19,7 @@ var util = require('./util'); var Promise = require('../util/promise').Promise; +var GcliFront = require('../connectors/remoted').GcliFront; /** * Markup a web page to highlight a collection of elements @@ -70,22 +71,20 @@ exports.Highlighter = Highlighter; */ exports.spawn = function(context, spawnSpec) { // Make sure we're only sending strings across XHR + var cmd = '' + spawnSpec.cmd; var cleanArgs = (spawnSpec.args || []).map(function(arg) { return '' + arg; }); + var cwd = '' + spawnSpec.cwd; var cleanEnv = Object.keys(spawnSpec.env || {}).reduce(function(prev, key) { prev[key] = '' + spawnSpec.env[key]; return prev; }, {}); - return context.system.connectors.get().connect().then(function(connection) { - return connection.call('system', { - cmd: '' + spawnSpec.cmd, - args: cleanArgs, - cwd: '' + spawnSpec.cwd, - env: cleanEnv - }).then(function(reply) { - connection.disconnect(); + var connector = context.system.connectors.get(); + return GcliFront.create(connector).then(function(front) { + return front.system(cmd, cleanArgs, cwd, cleanEnv).then(function(reply) { + front.connection.disconnect().then(null, util.errorHandler); return reply; }); }); From 7127930b176e3040559f1af4304d40a92cb3a795 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 17:39:18 +0000 Subject: [PATCH 30/42] runat-1128988: Remove rdp.js from GCLI/web tree It's still there in the mozmaster branch, but there's really no point in keeping it here because it doesn't work, and will never work. Signed-off-by: Joe Walker --- lib/gcli/connectors/rdp.js | 141 ------------------------------------- 1 file changed, 141 deletions(-) delete mode 100644 lib/gcli/connectors/rdp.js diff --git a/lib/gcli/connectors/rdp.js b/lib/gcli/connectors/rdp.js deleted file mode 100644 index cde7a8e1..00000000 --- a/lib/gcli/connectors/rdp.js +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -'use strict'; - -var Cu = require('chrome').Cu; - -var debuggerSocketConnect = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).debuggerSocketConnect; -var DebuggerClient = Cu.import('resource://gre/modules/devtools/dbg-client.jsm', {}).DebuggerClient; - -var Promise = require('../util/promise').Promise; -var Connection = require('./connectors').Connection; - -/** - * What port should we use by default? - */ -Object.defineProperty(exports, 'defaultPort', { - get: function() { - var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services; - try { - return Services.prefs.getIntPref('devtools.debugger.chrome-debugging-port'); - } - catch (ex) { - console.error('Can\'t use default port from prefs. Using 9999'); - return 9999; - } - }, - enumerable: true -}); - -exports.items = [ - { - item: 'connector', - name: 'rdp', - - connect: function(url) { - return RdpConnection.create(url); - } - } -]; - -/** - * RdpConnection uses the Firefox Remote Debug Protocol - */ -function RdpConnection(url) { - throw new Error('Use RdpConnection.create'); -} - -/** - * Asynchronous construction - */ -RdpConnection.create = function(url) { - this.host = url; - this.port = undefined; // TODO: Split out the port number - - this.requests = {}; - this.nextRequestId = 0; - - this._emit = this._emit.bind(this); - - return new Promise(function(resolve, reject) { - this.transport = debuggerSocketConnect(this.host, this.port); - this.client = new DebuggerClient(this.transport); - this.client.connect(function() { - this.client.listTabs(function(response) { - this.actor = response.gcliActor; - resolve(); - }.bind(this)); - }.bind(this)); - }.bind(this)); -}; - -RdpConnection.prototype = Object.create(Connection.prototype); - -RdpConnection.prototype.call = function(command, data) { - return new Promise(function(resolve, reject) { - var request = { to: this.actor, type: command, data: data }; - - this.client.request(request, function(response) { - resolve(response.commandSpecs); - }); - }.bind(this)); -}; - -RdpConnection.prototype.disconnect = function() { - return new Promise(function(resolve, reject) { - this.client.close(function() { - resolve(); - }); - - delete this._emit; - }.bind(this)); -}; - - -/** - * A Request is a command typed at the client which lives until the command - * has finished executing on the server - */ -function Request(actor, typed, args) { - this.json = { - to: actor, - type: 'execute', - typed: typed, - args: args, - requestId: 'id-' + Request._nextRequestId++, - }; - - this.promise = new Promise(function(resolve, reject) { - this._resolve = resolve; - }.bind(this)); -} - -Request._nextRequestId = 0; - -/** - * Called by the connection when a remote command has finished executing - * @param error boolean indicating output state - * @param type the type of the returned data - * @param data the data itself - */ -Request.prototype.complete = function(error, type, data) { - this._resolve({ - error: error, - type: type, - data: data - }); -}; From f48869601e299ec5c5324c653cbd4f387ef13af2 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 17:41:49 +0000 Subject: [PATCH 31/42] runat-1128988: Fix isRemote test detection isRemote is a testing flag to allow tests to be skipped if the commands are being run on a remote system. Previously there was some hacky voodoo which (predictably) broken, so now we're explicit about it. We also line up some comments in index.html. Signed-off-by: Joe Walker --- index.html | 4 ++-- lib/gcli/test/index.js | 4 ++-- remote.html | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index 9ca4f09e..e910da16 100644 --- a/index.html +++ b/index.html @@ -34,8 +34,8 @@ system.addItems(demo.items); // Extra demo commands gcli.createTerminal(system).then(function(terminal) { - terminal.language.showIntro(); // Intro text - test.run(terminal); // Run the unit test at each startup + terminal.language.showIntro(); // Intro text + test.run(terminal, false); // Run the unit test at each startup }).then(null, console.error.bind(console)); }); diff --git a/lib/gcli/test/index.js b/lib/gcli/test/index.js index a9ee2f1a..c71ccacf 100644 --- a/lib/gcli/test/index.js +++ b/lib/gcli/test/index.js @@ -69,7 +69,7 @@ var addDebugAids = exports.addDebugAids = function(options) { * - Runs the unit tests automatically on startup * - Registers a 'test' command to re-run the unit tests */ -exports.run = function(terminal) { +exports.run = function(terminal, isRemote) { var options = { terminal: terminal, window: window, @@ -78,7 +78,7 @@ exports.run = function(terminal) { isNode: false, isFirefox: false, isPhantomjs: (window.navigator.userAgent.indexOf('hantom') !== -1), - isRemote: (terminal.system.connection != null), + isRemote: isRemote, hideExec: true }; diff --git a/remote.html b/remote.html index 29530b41..d79a6a0a 100644 --- a/remote.html +++ b/remote.html @@ -30,7 +30,7 @@ var options = { connector: 'websocket' }; cnx.connect(options).then(function(system) { gcli.createTerminal(system).then(function(terminal) { - test.run(terminal); + test.run(terminal, true); }); }).then(null, console.error.bind(console)); }); From ce979a925def0614981eaac9ba765b21e80b7fe3 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 17:45:11 +0000 Subject: [PATCH 32/42] runat-1128988: A collection of nit fixes - Better error reporting in server.js - Remove unused require statements in direct.js - Add missing bind statements in remoted.js - Format a comment to help WebStorm do type detection Signed-off-by: Joe Walker --- lib/gcli/commands/server/server.js | 4 +--- lib/gcli/connectors/direct.js | 5 +---- lib/gcli/connectors/remoted.js | 4 ++-- lib/gcli/connectors/xhr.js | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/gcli/commands/server/server.js b/lib/gcli/commands/server/server.js index 3e26a33f..f9ff9a41 100644 --- a/lib/gcli/commands/server/server.js +++ b/lib/gcli/commands/server/server.js @@ -115,7 +115,6 @@ exports.items = [ }); }); - app.use('/lib', function(req, res) { var filename = main.gcliHome + '/lib' + url.parse(req.url).pathname; var webOverride = filename.replace(/\/lib\/gcli\//, '/web/gcli/'); @@ -257,10 +256,9 @@ var websocket = { socket.on(command, function(request) { // Handle errors from exceptions an promise rejections var onError = function(err) { - console.trace(); console.log('SOCKET ' + command + '(' + debugStr(request.data, 30) + ') Exception'); - console.error(err); + util.errorHandler(err); socket.emit('reply', { id: request.id, diff --git a/lib/gcli/connectors/direct.js b/lib/gcli/connectors/direct.js index e78b11ca..2913628c 100644 --- a/lib/gcli/connectors/direct.js +++ b/lib/gcli/connectors/direct.js @@ -16,12 +16,9 @@ 'use strict'; +var Promise = require('../util/promise').Promise; var createSystem = require('../system').createSystem; -var Commands = require('../commands/commands').Commands; -var Types = require('../types/types').Types; var Requisition = require('../cli').Requisition; -var Promise = require('../util/promise').Promise; - var Remoter = require('./remoted').Remoter; var Connection = require('./connectors').Connection; diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index 4a25f4d7..f1b23df0 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -158,7 +158,7 @@ Remoter.prototype.exposed = { var arg = assignment.arg; return arg == null ? undefined : arg.text; }); - }); + }.bind(this)); }, { request: { typed: Arg(0, "string"), // The command string @@ -177,7 +177,7 @@ Remoter.prototype.exposed = { var arg = assignment.arg; return arg == null ? undefined : arg.text; }); - }); + }.bind(this)); }, { request: { typed: Arg(0, "string"), // The command string diff --git a/lib/gcli/connectors/xhr.js b/lib/gcli/connectors/xhr.js index 78c37265..2926940e 100644 --- a/lib/gcli/connectors/xhr.js +++ b/lib/gcli/connectors/xhr.js @@ -49,7 +49,7 @@ XhrConnection.prototype = Object.create(Connection.prototype); * See server.js:remoted for details on the remoted functions * @param command The name of the exposed feature. * @param data The block of data to pass to the exposed feature - * @return A promise of the data returned by the remote feature + * @return Promise of the data returned by the remote feature */ XhrConnection.prototype.call = function(command, data) { return new Promise(function(resolve, reject) { From c7d092bd1a3d711e99fdb898cea39aa3e68e4c7c Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 17:48:05 +0000 Subject: [PATCH 33/42] runat-1128988: Split getSelectionInfo into getSelection[Lookup|Data] 2 separate methods make more sense than just one, and we're better off fixing this before it is a 'published' interface in Firefox. Signed-off-by: Joe Walker --- lib/gcli/connectors/remoted.js | 95 +++++++++++++++++++++++----------- lib/gcli/types/selection.js | 4 +- 2 files changed, 66 insertions(+), 33 deletions(-) diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index f1b23df0..d36afbde 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -189,44 +189,35 @@ Remoter.prototype.exposed = { /** * Perform a lookup on a selection type to get the allowed values */ - getSelectionInfo: method(function(commandName, paramName, action) { - var command = this.requisition.system.commands.get(commandName); - if (command == null) { - throw new Error('No command called \'' + commandName + '\''); - } - - var type; - command.params.forEach(function(param) { - if (param.name === paramName) { - type = param.type; - } - }); - if (type == null) { - throw new Error('No parameter called \'' + paramName + '\' in \'' + - commandName + '\''); - } + getSelectionLookup: method(function(commandName, paramName) { + var type = getType(this.requisition, commandName, paramName); var context = this.requisition.executionContext; + return type.lookup(context).map(function(info) { + // lookup returns an array of objects with name/value properties and + // the values might not be JSONable, so remove them + return { 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") + }), - switch (action) { - case 'lookup': - return type.lookup(context).map(function(info) { - // lookup returns an array of objects with name/value properties and - // the values might not be JSONable, so remove them - return { name: info.name }; - }); - - case 'data': - return type.data(context); + /** + * Perform a lookup on a selection type to get the allowed values + */ + getSelectionData: method(function(commandName, paramName) { + var type = getType(this.requisition, commandName, paramName); - default: - throw new Error('Action must be either \'lookup\' or \'data\''); - } + var context = this.requisition.executionContext; + return type.data(context); }, { request: { commandName: Arg(0, "string"), // The command containing the parameter in question - paramName: Arg(1, "string"), // The name of the parameter - action: Arg(2, "string") // 'lookup' or 'data' depending on the function to call + paramName: Arg(1, "string"), // The name of the parameter }, response: RetVal("json") }), @@ -282,6 +273,32 @@ Remoter.prototype.exposed = { }) }; +/** + * Helper for #getSelectionLookup and #getSelectionData that finds a type + * instance given a commandName and paramName + */ +function getType(requisition, commandName, paramName) { + var command = requisition.system.commands.get(commandName); + if (command == null) { + throw new Error('No command called \'' + commandName + '\''); + } + + var type; + command.params.forEach(function(param) { + if (param.name === paramName) { + type = param.type; + } + }); + + if (type == null) { + throw new Error('No parameter called \'' + paramName + '\' in \'' + + commandName + '\''); + } + + return type; +} + + /** * Asynchronous construction. Use GcliFront(); @@ -366,6 +383,22 @@ GcliFront.prototype.decrementType = function(typed, param) { return this.connection.call('decrementType', data); }; +GcliFront.prototype.getSelectionLookup = function(commandName, paramName) { + var data = { + commandName: commandName, + paramName: paramName + }; + return this.connection.call('getSelectionLookup', data); +}; + +GcliFront.prototype.getSelectionData = function(commandName, paramName) { + var data = { + commandName: commandName, + paramName: paramName + }; + return this.connection.call('getSelectionData', data); +}; + GcliFront.prototype.system = function(cmd, args, cwd, env) { var data = { cmd: cmd, diff --git a/lib/gcli/types/selection.js b/lib/gcli/types/selection.js index 4cd2c92f..1ab4fea7 100644 --- a/lib/gcli/types/selection.js +++ b/lib/gcli/types/selection.js @@ -126,11 +126,11 @@ SelectionType.prototype.getLookup = function(context) { var reply; if (this.remoteLookup) { - reply = this.front.getSelectionInfo('lookup', this.commandName, this.paramName); + reply = this.front.getSelectionLookup(this.commandName, this.paramName); reply = resolve(reply, context); } else if (this.remoteData) { - reply = this.front.getSelectionInfo('data', this.commandName, this.paramName); + reply = this.front.getSelectionData(this.commandName, this.paramName); reply = resolve(reply, context).then(this._dataToLookup); } else if (typeof this.lookup === 'function') { From fd63021d555d2cda031be57e5cdd5921040fa095 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 12 Feb 2015 17:50:24 +0000 Subject: [PATCH 34/42] runat-1128988: Create connection API: system.connectSystems There was connectors.connect, but that wasn't a great home for it, so we move it to a better place, and tidy it up a bit. Signed-off-by: Joe Walker --- lib/gcli/connectors/index.js | 138 ++++++++--------------------------- lib/gcli/system.js | 75 +++++++++++++++++++ remote.html | 3 +- 3 files changed, 107 insertions(+), 109 deletions(-) diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 83be7a4a..156c5c3a 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -17,127 +17,51 @@ 'use strict'; var createSystem = require('../system').createSystem; -var GcliFront = require('../connectors/remoted').GcliFront; +var connectSystems = require('../system').connectSystems; // Patch-up IE9 require('../util/legacy'); -/** - * - */ -var items = [ - require('../items/basic').items, - require('../items/ui').items, - require('../items/remote').items, - - // TODO: why is this here? - require('../commands/context').items, - -].reduce(function(prev, curr) { return prev.concat(curr); }, []); - -/** - * These are the commands stored on the remote side that have converters which - * we'll need to present the data. Ideally front.specs() would transfer these, - * that doesn't happen yet so we add them manually - */ -var requiredConverters = [ - require('../cli').items, - - require('../commands/clear').items, - require('../commands/connect').items, - require('../commands/exec').items, - require('../commands/global').items, - require('../commands/help').items, - require('../commands/intro').items, - require('../commands/lang').items, - require('../commands/preflist').items, - require('../commands/pref').items, - require('../commands/test').items, - -].reduce(function(prev, curr) { return prev.concat(curr); }, []) - .filter(function(item) { return item.item === 'converter'; }); - /** * Connect to a remote system and setup the commands/types/converters etc needed * to make it all work */ -exports.connect = function(options) { +exports.createSystem = function(options) { options = options || {}; var system = createSystem(); + + // The items that are always needed on the client + var items = [ + require('../items/basic').items, + require('../items/ui').items, + require('../items/remote').items, + // The context command makes no sense on the server + require('../commands/context').items, + ].reduce(function(prev, curr) { return prev.concat(curr); }, []); system.addItems(items); + + // These are the commands stored on the remote side that have converters which + // we'll need to present the data. Ideally front.specs() would transfer these, + // that doesn't happen yet so we add them manually + var requiredConverters = [ + require('../cli').items, + require('../commands/clear').items, + require('../commands/connect').items, + require('../commands/exec').items, + require('../commands/global').items, + require('../commands/help').items, + require('../commands/intro').items, + require('../commands/lang').items, + require('../commands/preflist').items, + require('../commands/pref').items, + require('../commands/test').items, + ].reduce(function(prev, curr) { return prev.concat(curr); }, []) + .filter(function(item) { return item.item === 'converter'; }); system.addItems(requiredConverters); var connector = system.connectors.get(options.method); - GcliFront.create(connector, options.url).then(function(front) { - front.on('commandsChanged', function(specs) { - exports.addItems(system, specs, front); - }); - - return front.specs().then(function(specs) { - exports.addItems(system, specs, front); - return system; - }); - }.bind(this)); -}; - -exports.addItems = function(system, specs, front) { - exports.removeRemoteItems(system, front); - var remoteItems = exports.addLocalFunctions(specs, front); - system.addItems(remoteItems); -}; - -/** - * Take the data from the 'specs' command (or the 'commandsChanged' event) and - * add function to proxy the execution back over the front - */ -exports.addLocalFunctions = function(specs, front) { - // Inject an 'exec' function into the commands, and the front into - // all the remote types - specs.forEach(function(commandSpec) { - // HACK: Tack the front to the command so we know how to remove it - // in removeRemoteItems() below - commandSpec.front = front; - - // TODO: removeRemoteItems() doesn't remove types, so do we need this? - commandSpec.params.forEach(function(param) { - if (typeof param.type !== 'string') { - param.type.front = front; - } - }); - - if (!commandSpec.isParent) { - commandSpec.exec = function(args, context) { - var data = { - typed: (context.prefix ? context.prefix + ' ' : '') + context.typed - }; - - return front.execute(data).then(function(reply) { - var typedData = context.typedData(reply.type, reply.data); - if (!reply.error) { - return typedData; - } - else { - throw typedData; - } - }); - }; - } - - commandSpec.isProxy = true; - }); - - return specs; -}; - -/** - * Go through all the commands removing any that are associated with the given - * front. The method of association is the hack in addLocalFunctions. - */ -exports.removeRemoteItems = function(system, front) { - system.commands.getAll().forEach(function(command) { - if (command.front === front) { - system.commands.remove(command); - } + return connectSystems(system, connector, options.url).then(function() { + return system; }); }; diff --git a/lib/gcli/system.js b/lib/gcli/system.js index f0da194c..1cd4655e 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -24,6 +24,7 @@ var Fields = require('./fields/fields').Fields; var Languages = require('./languages/languages').Languages; var Settings = require('./settings').Settings; var Types = require('./types/types').Types; +var GcliFront = require('./connectors/remoted').GcliFront; /** * This is the heart of the API that we expose to the outside. @@ -239,3 +240,77 @@ exports.createSystem = function(options) { return system; }; + +/** + * Connect a local system with another at the other end of a connector + */ +exports.connectSystems = function(system, connector, url) { + return GcliFront.create(connector, url).then(function(front) { + front.on('commandsChanged', function(specs) { + syncItems(system, specs, front); + }); + + return front.specs().then(function(specs) { + syncItems(system, specs, front); + return system; + }); + }); +}; + +/** + * Remove the items in this system that came from a previous sync action, and + * re-add them + */ +function syncItems(system, specs, front) { + // Go through all the commands removing any that are associated with the given + // front. The method of association is the hack in addLocalFunctions. + system.commands.getAll().forEach(function(command) { + if (command.front === front) { + system.commands.remove(command); + } + }); + + var remoteItems = addLocalFunctions(specs, front); + system.addItems(remoteItems); +} + +/** + * Take the data from the 'specs' command (or the 'commandsChanged' event) and + * add function to proxy the execution back over the front + */ +function addLocalFunctions(specs, front) { + // Inject an 'exec' function into the commands, and the front into + // all the remote types + specs.forEach(function(commandSpec) { + // HACK: Tack the front to the command so we know how to remove it + // in syncItems() below + commandSpec.front = front; + + // TODO: syncItems() doesn't remove types, so do we need this? + commandSpec.params.forEach(function(param) { + if (typeof param.type !== 'string') { + param.type.front = front; + } + }); + + if (!commandSpec.isParent) { + commandSpec.exec = function(args, context) { + var typed = (context.prefix ? context.prefix + ' ' : '') + context.typed; + + return front.execute(typed).then(function(reply) { + var typedData = context.typedData(reply.type, reply.data); + if (!reply.error) { + return typedData; + } + else { + throw typedData; + } + }); + }; + } + + commandSpec.isProxy = true; + }); + + return specs; +} diff --git a/remote.html b/remote.html index d79a6a0a..ca74fcd0 100644 --- a/remote.html +++ b/remote.html @@ -27,8 +27,7 @@ }); require([ 'gcli/index', 'gcli/test/index', 'gcli/connectors/index' ], function(gcli, test, cnx) { - var options = { connector: 'websocket' }; - cnx.connect(options).then(function(system) { + cnx.createSystem({ connector: 'websocket' }).then(function(system) { gcli.createTerminal(system).then(function(terminal) { test.run(terminal, true); }); From 166ec79bb1bb7b28d230adf23fb7a33b6f06e417 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 13 Feb 2015 18:30:18 +0000 Subject: [PATCH 35/42] runat-1128988: 'system' should use a GcliFront not a connector GCLI in general should rely on a front rather than a connector which is a much lower level thing. Signed-off-by: Joe Walker --- lib/gcli/connectors/index.js | 9 ++++++--- lib/gcli/system.js | 17 +++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 156c5c3a..2e410879 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -17,7 +17,8 @@ 'use strict'; var createSystem = require('../system').createSystem; -var connectSystems = require('../system').connectSystems; +var connectFront = require('../system').connectFront; +var GcliFront = require('./connectors/remoted').GcliFront; // Patch-up IE9 require('../util/legacy'); @@ -61,7 +62,9 @@ exports.createSystem = function(options) { system.addItems(requiredConverters); var connector = system.connectors.get(options.method); - return connectSystems(system, connector, options.url).then(function() { - return system; + return GcliFront.create(connector, options.url).then(function(front) { + return connectFront(system, front).then(function() { + return system; + }); }); }; diff --git a/lib/gcli/system.js b/lib/gcli/system.js index 1cd4655e..034bf501 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -24,7 +24,6 @@ var Fields = require('./fields/fields').Fields; var Languages = require('./languages/languages').Languages; var Settings = require('./settings').Settings; var Types = require('./types/types').Types; -var GcliFront = require('./connectors/remoted').GcliFront; /** * This is the heart of the API that we expose to the outside. @@ -244,16 +243,14 @@ exports.createSystem = function(options) { /** * Connect a local system with another at the other end of a connector */ -exports.connectSystems = function(system, connector, url) { - return GcliFront.create(connector, url).then(function(front) { - front.on('commandsChanged', function(specs) { - syncItems(system, specs, front); - }); +exports.connectFront = function(system, front) { + front.on('commandsChanged', function(specs) { + syncItems(system, specs, front); + }); - return front.specs().then(function(specs) { - syncItems(system, specs, front); - return system; - }); + return front.specs().then(function(specs) { + syncItems(system, specs, front); + return system; }); }; From e2a1c6aa642898658dc6ce6035a3351bd01d0655 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 13 Feb 2015 18:31:20 +0000 Subject: [PATCH 36/42] runat-1128988: Be less brutal with errors When registering modules it's bad to take the nuclear option because then nothing works on a small error, so we log instead. Signed-off-by: Joe Walker --- lib/gcli/system.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gcli/system.js b/lib/gcli/system.js index 034bf501..fd227e7f 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -127,8 +127,8 @@ exports.createSystem = function(options) { }); } catch (ex) { - console.error(ex); - return Promise.reject('Failure when loading \'' + name + '\''); + console.error('Failed to load module ' + name + ': ' + ex); + console.error(ex.stack); } }; From 0691a90f62db5caa9468cc10ba6c35582725e4f5 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Fri, 13 Feb 2015 19:46:20 +0000 Subject: [PATCH 37/42] runat-1128988: Use `function*() ...` for generators See https://github.com/joewalker/gcli/pull/75 Signed-off-by: Joe Walker --- lib/gcli/commands/server/firefox.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gcli/commands/server/firefox.js b/lib/gcli/commands/server/firefox.js index 488dbb61..f7fc0ce0 100644 --- a/lib/gcli/commands/server/firefox.js +++ b/lib/gcli/commands/server/firefox.js @@ -136,7 +136,7 @@ function createCommonJsToJsTestFilter() { 'var TEST_URI = "data:text/html;charset=utf-8,

gcli-' + name + '

";\n' + '\n' + 'function test() {\n' + - ' return Task.spawn(function() {\n' + + ' return Task.spawn(*function() {\n' + ' let options = yield helpers.openTab(TEST_URI);\n' + ' yield helpers.openToolbar(options);\n' + ' options.requisition.system.addItems(mockCommands.items);\n' + From cd39f733a2299e115442c8d9971df4004ee2e3be Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Sun, 15 Feb 2015 12:45:38 +0000 Subject: [PATCH 38/42] runat-1128988: Improve support for custom properties for commands GCLI has allowed (by turning a blind eye) to commands and other items that have additional properties. This is used by Firefox to allow commands to be placed in the toolbox and toolbar buttons with custom icons. With remote GCLI it's important that these custom properties are transferred along with the standard command metadata. This change allows the 'specs' function to take an array of custom properties which should be transferred. Signed-off-by: Joe Walker --- lib/gcli/commands/commands.js | 22 ++++++++++++++--- lib/gcli/connectors/index.js | 2 +- lib/gcli/connectors/remoted.js | 11 ++++++--- lib/gcli/system.js | 44 ++++++++++++++++++++-------------- lib/gcli/test/testCanon.js | 21 ++++++++++++++++ 5 files changed, 75 insertions(+), 25 deletions(-) diff --git a/lib/gcli/commands/commands.js b/lib/gcli/commands/commands.js index 0cc4cfbb..e76ea8b7 100644 --- a/lib/gcli/commands/commands.js +++ b/lib/gcli/commands/commands.js @@ -161,8 +161,11 @@ function Command(types, commandSpec) { /** * JSON serializer that avoids non-serializable data + * @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. */ -Command.prototype.toJson = function() { +Command.prototype.toJson = function(customProps) { var json = { item: 'command', name: this.name, @@ -170,6 +173,7 @@ Command.prototype.toJson = function() { returnType: this.returnType, isParent: (this.exec == null) }; + if (this.description !== l10n.lookup('canonDescNone')) { json.description = this.description; } @@ -179,6 +183,15 @@ Command.prototype.toJson = function() { if (this.hidden != null) { json.hidden = this.hidden; } + + if (Array.isArray(customProps)) { + customProps.forEach(function(prop) { + if (this[prop] != null) { + json[prop] = this[prop]; + } + }.bind(this)); + } + return json; }; @@ -425,14 +438,17 @@ Commands.prototype.getAll = function() { /** * Get access to the stored commandMetaDatas (i.e. before they were made into * instances of Command/Parameters) so we can remote them. + * @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. */ -Commands.prototype.getCommandSpecs = function() { +Commands.prototype.getCommandSpecs = function(customProps) { var commandSpecs = []; Object.keys(this._commands).forEach(function(name) { var command = this._commands[name]; if (!command.noRemote) { - commandSpecs.push(command.toJson()); + commandSpecs.push(command.toJson(customProps)); } }.bind(this)); diff --git a/lib/gcli/connectors/index.js b/lib/gcli/connectors/index.js index 2e410879..9f98e9e2 100644 --- a/lib/gcli/connectors/index.js +++ b/lib/gcli/connectors/index.js @@ -18,7 +18,7 @@ var createSystem = require('../system').createSystem; var connectFront = require('../system').connectFront; -var GcliFront = require('./connectors/remoted').GcliFront; +var GcliFront = require('./remoted').GcliFront; // Patch-up IE9 require('../util/legacy'); diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index d36afbde..4d4fce0b 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -78,11 +78,16 @@ Remoter.prototype.removeListener = function(action) { Remoter.prototype.exposed = { /** * 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() { - return this.requisition.system.commands.getCommandSpecs(); + specs: method(function(customProps) { + return this.requisition.system.commands.getCommandSpecs(customProps); }, { - request: {}, + request: { + customProps: Arg(0, "nullable:array:string") + }, response: RetVal("json") }), diff --git a/lib/gcli/system.js b/lib/gcli/system.js index fd227e7f..0bbc5437 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -17,6 +17,7 @@ 'use strict'; var Promise = require('./util/promise').Promise; +var util = require('./util/util'); var Commands = require('./commands/commands').Commands; var Connectors = require('./connectors/connectors').Connectors; var Converters = require('./converters/converters').Converters; @@ -242,34 +243,41 @@ exports.createSystem = function(options) { /** * Connect a local system with another at the other end of a connector + * @param system System to which we're adding commands + * @param front Front which allows access to the remote system from which we + * import commands + * @param customProps Array of strings specifying additional properties defined + * on remote commands that should be considered part of the metadata for the + * commands imported into the local system */ -exports.connectFront = function(system, front) { +exports.connectFront = function(system, front, customProps) { front.on('commandsChanged', function(specs) { - syncItems(system, specs, front); + syncItems(system, front, customProps).then(null, util.errorHandler); }); - return front.specs().then(function(specs) { - syncItems(system, specs, front); - return system; - }); + return syncItems(system, front, customProps); }; /** * Remove the items in this system that came from a previous sync action, and - * re-add them + * re-add them. See connectFront() for explanation of properties */ -function syncItems(system, specs, front) { - // Go through all the commands removing any that are associated with the given - // front. The method of association is the hack in addLocalFunctions. - system.commands.getAll().forEach(function(command) { - if (command.front === front) { - system.commands.remove(command); - } - }); +function syncItems(system, front, customProps) { + return front.specs(customProps).then(function(specs) { + // Go through all the commands removing any that are associated with the + // given front. The method of association is the hack in addLocalFunctions. + system.commands.getAll().forEach(function(command) { + if (command.front === front) { + system.commands.remove(command); + } + }); - var remoteItems = addLocalFunctions(specs, front); - system.addItems(remoteItems); -} + var remoteItems = addLocalFunctions(specs, front); + system.addItems(remoteItems); + + return system; + }); +}; /** * Take the data from the 'specs' command (or the 'commandsChanged' event) and diff --git a/lib/gcli/test/testCanon.js b/lib/gcli/test/testCanon.js index f223db76..938d3062 100644 --- a/lib/gcli/test/testCanon.js +++ b/lib/gcli/test/testCanon.js @@ -195,6 +195,9 @@ exports.testAltCommands = function(options) { { name: 'num', type: 'number' }, { name: 'opt', type: { name: 'selection', data: [ '1', '2', '3' ] } }, ], + customProp1: 'localValue', + customProp2: true, + customProp3: 42, exec: function(args, context) { return context.commandName + ':' + args.str + ':' + args.num + ':' + args.opt; @@ -211,6 +214,24 @@ exports.testAltCommands = function(options) { '],"isParent":false}]', 'JSON.stringify(commandSpecs)'); + var customProps = [ 'customProp1', 'customProp2', 'customProp3', ]; + var commandSpecs2 = altCommands.getCommandSpecs(customProps); + assert.is(JSON.stringify(commandSpecs2), + '[{' + + '"item":"command",' + + '"name":"tss",' + + '"params":[' + + '{"name":"str","type":"string"},' + + '{"name":"num","type":"number"},' + + '{"name":"opt","type":{"name":"selection","data":["1","2","3"]}}' + + '],' + + '"isParent":false,' + + '"customProp1":"localValue",' + + '"customProp2":true,' + + '"customProp3":42' + + '}]', + 'JSON.stringify(commandSpecs)'); + var remoter = function(args, context) { assert.is(context.commandName, 'tss', 'commandName is tss'); From 969e9da3b699bb177e313accab4f659481d6f745 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Mar 2015 08:49:33 +0000 Subject: [PATCH 39/42] runat-1128988: Use .catch(...) in place of .then(null, ...) I thought that catch() died with end(), but apparently not. This has the downside that Eclipse/JSDT mistakes the function 'catch' for the keyword of the same name, so all parsing fails, but I don't think we should hold back code for the sake of broken editors. Signed-off-by: Joe Walker --- gcli.js | 8 +++----- index.html | 2 +- lib/gcli/commands/server/server.js | 2 +- lib/gcli/fields/selection.js | 4 ++-- lib/gcli/languages/command.js | 6 +++--- lib/gcli/languages/languages.js | 2 +- lib/gcli/system.js | 4 ++-- lib/gcli/test/index.js | 2 +- lib/gcli/ui/terminal.js | 6 +++--- lib/gcli/util/util.js | 2 +- remote.html | 2 +- web/gcli/types/fileparser.js | 2 +- web/gcli/util/host.js | 2 +- 13 files changed, 21 insertions(+), 23 deletions(-) diff --git a/gcli.js b/gcli.js index 911d052a..eb544be8 100644 --- a/gcli.js +++ b/gcli.js @@ -79,7 +79,7 @@ function logResults(output) { requisition.updateExec(command) .then(logResults) .then(extraActions) - .then(null, util.errorHandler); + .catch(util.errorHandler); /** * Start a NodeJS REPL to execute commands @@ -96,10 +96,8 @@ function startRepl() { if (command.length !== 0) { requisition.updateExec(command) .then(logResults) - .then( - function() { callback(); }, - function(ex) { util.errorHandler(ex); callback(); } - ); + .then(function() { callback(); }) + .catch(function(ex) { util.errorHandler(ex); callback(); }); } }; diff --git a/index.html b/index.html index e910da16..918264f9 100644 --- a/index.html +++ b/index.html @@ -36,7 +36,7 @@ gcli.createTerminal(system).then(function(terminal) { terminal.language.showIntro(); // Intro text test.run(terminal, false); // Run the unit test at each startup - }).then(null, console.error.bind(console)); + }).catch(console.error.bind(console)); }); diff --git a/lib/gcli/commands/server/server.js b/lib/gcli/commands/server/server.js index f9ff9a41..45494228 100644 --- a/lib/gcli/commands/server/server.js +++ b/lib/gcli/commands/server/server.js @@ -192,7 +192,7 @@ var xhrsocket = { res.status(500).send(text); }; - return Promise.resolve(reply).then(onResolve).then(null, onReject); + return Promise.resolve(reply).then(onResolve).catch(onReject); }); }); } diff --git a/lib/gcli/fields/selection.js b/lib/gcli/fields/selection.js index 1aa45f6e..d3e7597b 100644 --- a/lib/gcli/fields/selection.js +++ b/lib/gcli/fields/selection.js @@ -75,7 +75,7 @@ SelectionField.prototype.setConversion = function(conversion) { prediction; }, this); this.menu.show(items, conversion.arg.text); - }.bind(this), util.errorHandler); + }.bind(this)).catch(util.errorHandler); }; SelectionField.prototype.itemClicked = function(ev) { @@ -85,7 +85,7 @@ SelectionField.prototype.itemClicked = function(ev) { this.type.parse(arg, context).then(function(conversion) { this.onFieldChange({ conversion: conversion }); this.setMessage(conversion.message); - }.bind(this)).then(null, util.errorHandler); + }.bind(this)).catch(util.errorHandler); }; SelectionField.prototype.getConversion = function() { diff --git a/lib/gcli/languages/command.js b/lib/gcli/languages/command.js index 0a950c8e..33ff8e05 100644 --- a/lib/gcli/languages/command.js +++ b/lib/gcli/languages/command.js @@ -184,7 +184,7 @@ var commandLanguage = exports.commandLanguage = { var isNew = (this.assignment !== newAssignment); this.assignment = newAssignment; - this.terminal.updateCompletion().then(null, util.errorHandler); + this.terminal.updateCompletion().catch(util.errorHandler); if (isNew) { this.updateHints(); @@ -286,7 +286,7 @@ var commandLanguage = exports.commandLanguage = { } this.terminal.history.add(input); - this.terminal.unsetChoice().then(null, util.errorHandler); + this.terminal.unsetChoice().catch(util.errorHandler); this.terminal.inputElement.value = ''; this.terminal_previousValue = this.terminal.inputElement.value; @@ -499,7 +499,7 @@ var commandLanguage = exports.commandLanguage = { this.terminal.scrollToBottom(); data.throbEle.style.display = ev.output.completed ? 'none' : 'block'; }.bind(this)); - }.bind(this)).then(null, console.error); + }.bind(this)).catch(console.error); this.terminal.addElement(data.rowinEle); this.terminal.addElement(data.rowoutEle); diff --git a/lib/gcli/languages/languages.js b/lib/gcli/languages/languages.js index 738c7f94..3c5e8845 100644 --- a/lib/gcli/languages/languages.js +++ b/lib/gcli/languages/languages.js @@ -82,7 +82,7 @@ var baseLanguage = { this.focusManager.outputted(); - this.terminal.unsetChoice().then(null, util.errorHandler); + this.terminal.unsetChoice().catch(util.errorHandler); this.terminal.inputElement.value = ''; }.bind(this)); }, diff --git a/lib/gcli/system.js b/lib/gcli/system.js index 0bbc5437..7841b690 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -161,7 +161,7 @@ exports.createSystem = function(options) { pendingChanges = true; } else { - loadModule(name).then(null, console.error); + loadModule(name).catch(console.error); } }); }, @@ -252,7 +252,7 @@ exports.createSystem = function(options) { */ exports.connectFront = function(system, front, customProps) { front.on('commandsChanged', function(specs) { - syncItems(system, front, customProps).then(null, util.errorHandler); + syncItems(system, front, customProps).catch(util.errorHandler); }); return syncItems(system, front, customProps); diff --git a/lib/gcli/test/index.js b/lib/gcli/test/index.js index c71ccacf..b49387b2 100644 --- a/lib/gcli/test/index.js +++ b/lib/gcli/test/index.js @@ -110,7 +110,7 @@ exports.run = function(terminal, isRemote) { console.log(helpers.timingSummary); document.testStatus = examiner.status.name; - setMocks(options, false).then(closeIfPhantomJs).then(null, function(ex) { + setMocks(options, false).then(closeIfPhantomJs).catch(function(ex) { console.error(ex); closeIfPhantomJs(); }); diff --git a/lib/gcli/ui/terminal.js b/lib/gcli/ui/terminal.js index 8a3abbbd..8ddf687e 100644 --- a/lib/gcli/ui/terminal.js +++ b/lib/gcli/ui/terminal.js @@ -266,7 +266,7 @@ Terminal.prototype._updateLanguage = function(language) { } this.language.updateHints(); - this.updateCompletion().then(null, util.errorHandler); + this.updateCompletion().catch(util.errorHandler); this.promptElement.innerHTML = this.language.prompt; }; @@ -393,7 +393,7 @@ Terminal.prototype.onKeyDown = function(ev) { * if something went wrong. */ Terminal.prototype.onKeyUp = function(ev) { - this.handleKeyUp(ev).then(null, util.errorHandler); + this.handleKeyUp(ev).catch(util.errorHandler); }; /** @@ -590,7 +590,7 @@ Terminal.prototype.updateCompletion = function() { * event handlers that can't react to rejected promises. */ Terminal.prototype._updateCompletionWithErrorHandler = function() { - this.updateCompletion().then(null, util.errorHandler); + this.updateCompletion().catch(util.errorHandler); }; /** diff --git a/lib/gcli/util/util.js b/lib/gcli/util/util.js index 800e416c..4e205f4b 100644 --- a/lib/gcli/util/util.js +++ b/lib/gcli/util/util.js @@ -275,7 +275,7 @@ exports.promiseEach = function(array, action, scope) { }; var reply = action.call(scope, array[index], index, array); - Promise.resolve(reply).then(onSuccess).then(null, onFailure); + Promise.resolve(reply).then(onSuccess).catch(onFailure); }; callNext(0); diff --git a/remote.html b/remote.html index ca74fcd0..987d105f 100644 --- a/remote.html +++ b/remote.html @@ -31,7 +31,7 @@ gcli.createTerminal(system).then(function(terminal) { test.run(terminal, true); }); - }).then(null, console.error.bind(console)); + }).catch(console.error.bind(console)); }); diff --git a/web/gcli/types/fileparser.js b/web/gcli/types/fileparser.js index 908df045..969c92b1 100644 --- a/web/gcli/types/fileparser.js +++ b/web/gcli/types/fileparser.js @@ -52,7 +52,7 @@ exports.parse = function(context, typed, options) { return Promise.resolve(reply.predictions); }; } - front.connection.disconnect().then(null, util.errorHandler); + front.connection.disconnect().catch(util.errorHandler); return reply; }); }); diff --git a/web/gcli/util/host.js b/web/gcli/util/host.js index f535ebb4..d4d06483 100644 --- a/web/gcli/util/host.js +++ b/web/gcli/util/host.js @@ -84,7 +84,7 @@ exports.spawn = function(context, spawnSpec) { var connector = context.system.connectors.get(); return GcliFront.create(connector).then(function(front) { return front.system(cmd, cleanArgs, cwd, cleanEnv).then(function(reply) { - front.connection.disconnect().then(null, util.errorHandler); + front.connection.disconnect().catch(util.errorHandler); return reply; }); }); From b62acdf1914ac09b39f203d177adfaf9eb233c08 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Mar 2015 09:12:20 +0000 Subject: [PATCH 40/42] runat-1128988: Protect from NPE in debug code See review comment: https://github.com/joewalker/gcli/commit/dba90086ccb0df32c49589e95d1218758dd29956 Signed-off-by: Joe Walker --- lib/gcli/system.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gcli/system.js b/lib/gcli/system.js index 7841b690..c62f8d8a 100644 --- a/lib/gcli/system.js +++ b/lib/gcli/system.js @@ -70,7 +70,9 @@ exports.createSystem = function(options) { components[getItemType(item)].add(item); } catch (ex) { - console.error('While adding: ' + item.name); + if (item != null) { + console.error('While adding: ' + item.name); + } throw ex; } }; From 9c1f3880bfb19f8b70d695eda7cc42f5a5dfe9ce Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Mar 2015 09:22:52 +0000 Subject: [PATCH 41/42] runat-1128988: Fix typo in variable name and statement ordering See review comment at https://github.com/joewalker/gcli/commit/7a95a196ab10fa29adfa8b216c1293b56365536c Signed-off-by: Joe Walker --- lib/gcli/languages/command.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gcli/languages/command.js b/lib/gcli/languages/command.js index 33ff8e05..e8ec20af 100644 --- a/lib/gcli/languages/command.js +++ b/lib/gcli/languages/command.js @@ -288,8 +288,8 @@ var commandLanguage = exports.commandLanguage = { this.terminal.history.add(input); this.terminal.unsetChoice().catch(util.errorHandler); + this.terminal._previousValue = this.terminal.inputElement.value; this.terminal.inputElement.value = ''; - this.terminal_previousValue = this.terminal.inputElement.value; return this.requisition.exec().then(function() { this.textChanged(); From 0ed2dc744b30effa740ea9fc74b72f39569350e9 Mon Sep 17 00:00:00 2001 From: Joe Walker Date: Thu, 5 Mar 2015 09:26:20 +0000 Subject: [PATCH 42/42] runat-1128988: Fix duplicate comments See review comment: https://github.com/joewalker/gcli/commit/c7d092bd1a3d711e99fdb898cea39aa3e68e4c7c#commitcomment-9884204 Signed-off-by: Joe Walker --- lib/gcli/connectors/remoted.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gcli/connectors/remoted.js b/lib/gcli/connectors/remoted.js index 4d4fce0b..92cc3586 100644 --- a/lib/gcli/connectors/remoted.js +++ b/lib/gcli/connectors/remoted.js @@ -192,7 +192,7 @@ Remoter.prototype.exposed = { }), /** - * Perform a lookup on a selection type to get the allowed values + * Call type.lookup() on a selection type to get the allowed values */ getSelectionLookup: method(function(commandName, paramName) { var type = getType(this.requisition, commandName, paramName); @@ -212,7 +212,7 @@ Remoter.prototype.exposed = { }), /** - * Perform a lookup on a selection type to get the allowed values + * Call type.data() on a selection type to get the allowed values */ getSelectionData: method(function(commandName, paramName) { var type = getType(this.requisition, commandName, paramName);