diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index cacfce70..6391844a 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -96,10 +96,6 @@ contains certain features to ease communication: - [Well-documented exit codes.][exit-codes] -- An environment variable called `ETCHER_CLI_ROBOT` option, which when set - causes the Etcher CLI to output state in a way that can be easily - parsed by a machine. - Summary ------- diff --git a/lib/gui/app/components/safe-webview.js b/lib/gui/app/components/safe-webview.js index b9f2af11..9b457190 100644 --- a/lib/gui/app/components/safe-webview.js +++ b/lib/gui/app/components/safe-webview.js @@ -26,7 +26,6 @@ const propTypes = require('prop-types') const { react2angular } = require('react2angular') const analytics = require('../modules/analytics') const packageJSON = require('../../../../package.json') -const robot = require('../../../shared/robot') const MODULE_NAME = 'Etcher.Components.SafeWebview' const angularSafeWebview = angular.module(MODULE_NAME, []) @@ -221,27 +220,24 @@ class SafeWebview extends react.PureComponent { * @example * * // In the webview - * console.log(JSON.stringify({ - * command: 'error', - * data: 'Good night!' - * })); - * - * console.log(JSON.stringify({ - * command: 'log', - * data: 'Hello, Mars!' - * })); + * console.log('Good night!') */ static consoleMessage (event) { - if (!robot.isMessage(event.message)) { + if (_.isNil(event.message)) { return } - const message = robot.parseMessage(event.message) + let message = event.message + try { + message = JSON.parse(event.message) + } catch (error) { + // Ignore + } - if (robot.getCommand(message) === robot.COMMAND.LOG) { - analytics.logEvent(robot.getData(message)) - } else if (robot.getCommand(message) === robot.COMMAND.ERROR) { - analytics.logException(robot.getData(message)) + if (message.command === 'error') { + analytics.logException(message.data) + } else { + analytics.logEvent(message.data || message) } } } diff --git a/lib/shared/robot/README.md b/lib/shared/robot/README.md deleted file mode 100644 index 8930098d..00000000 --- a/lib/shared/robot/README.md +++ /dev/null @@ -1,132 +0,0 @@ -The "robot" mechanism -===================== - -The "robot" module is an entity that implements a text-based protocol to share -objects between processes. - -The contents and structure of these messages is what the "robot" module is -mainly concerned with. Each "message" consists of a type (a "command" in robot -parlance) and an arbitrary data object: - -- `String command`: the message command name -- `Object data`: the message data - -For example: - -*Child process:* - -```js -robot.printMessage('my-message-type', { - my: { - message: 'data' - } -}); -``` - -*Parent process:* - -```js -const message = robot.parseMessage(line); - -console.log(robot.getCommand(message)); -> 'my-message-type' - -console.log(robot.getData(message)); -> { -> my: { -> message: 'data' -> } -> } -``` - -**Logging debug data to the console:** - -*Child process:* - -```js -// This will log the passed data to parent's console, -// as `console.log()`ing in the child will cause errors -robot.log({ debugging: 'things' }) -``` - -The codename "robot" is inspired by [xz][xz-man], which provides a `--robot` -option that makes the tool print machine-parseable output: - -``` ---robot - Print messages in a machine-parsable format. This is intended - to ease writing frontends that want to use xz instead of - liblzma, which may be the case with various scripts. The output - with this option enabled is meant to be stable across xz - releases. See the section ROBOT MODE for details. -``` - -To enable the "robot" option, we standardised the presence of an -`ETCHER_CLI_ROBOT` environment variable. You can check if the mode is enabled -by using the `.isEnabled()` static function that the robot module provides: - -```js -if (robot.isEnabled()) { - console.log('The robot option is enabled'); -} -``` - -The current protocol that we use is based on JSON. The writer process -stringifies a JSON object, and prints it. The client then gets the line, parses -it as JSON, and accesses the object. - -For example, the writer process may have a fictitious internal object that -looks like this: - -```js -{ - percentage: 50, - stage: 'validation' -} -``` - -That object can be stringified as `{"percentage":50,"stage":"validation"}` and -printed to `stdout`. - -This is what a valid robot message looks like: - -```json -{ - "command": "progress", - "data": { - "percentage": 50 - } -} -``` - -The command content and the data associated with it are application specific, -however the robot module defines a single command called "error", which is used -to transmit a JavaScript Error object, along with its metadata (stacktrace, -code, description, etc) as a string. - -You don't have to worry about the internal details of how an Error object is -encoded/decoded, given that the robot module exposes two high level utility -functions: `.printError()` and `.recomposeErrorMessage()`. - -Here's an example of these functions in action: - -```javascript -const error = errors.createError({ - title: 'This is an error', - description: 'My description' -}); - -robot.printError(error); -``` - -The client can then fetch the line, and recompose it back: - -```javascript -const error = robot.recomposeErrorMessage(line); -``` - -The resulting `error` inherits the stacktrace and other metadata from the -original error, even if this was created in another process. This is how the -writer process propagates informational errors to the GUI. - -[xz-man]: https://www.freebsd.org/cgi/man.cgi?query=xz&sektion=1&manpath=FreeBSD+8.3-RELEASE diff --git a/lib/shared/robot/index.js b/lib/shared/robot/index.js deleted file mode 100644 index 9a16f245..00000000 --- a/lib/shared/robot/index.js +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2016 resin.io - * - * 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' - -const _ = require('lodash') -const errors = require('../errors') - -/** - * @summary Robot commands - * @namespace COMMAND - * @public - */ -exports.COMMAND = { - - /** - * @property {String} ERROR - * @memberof COMMAND - * - * @description - * This command is used to represent an error message. - */ - ERROR: 'error', - - /** - * @property {String} LOG - * @memberof COMMAND - * - * @description - * This command is used to represent a log message. - */ - LOG: 'log' - -} - -/** - * @summary Check whether we should emit parseable output - * @function - * @public - * - * @param {Object} environment - environment - * @returns {Boolean} whether we should emit parseable output - * - * @example - * if (robot.isEnabled(process.env)) { - * console.log('We should emit parseable output'); - * } - */ -exports.isEnabled = (environment) => { - const value = _.get(environment, [ 'ETCHER_CLI_ROBOT' ], false) - return Boolean(value === 'false' ? false : value) -} - -/** - * @summary Build a machine-parseable message - * @function - * @private - * - * @param {String} title - message title - * @param {Object} [data] - message data - * @returns {String} parseable message - * - * @example - * const message = robot.buildMessage('progress', { - * percentage: 50 - * }); - * - * console.log(message); - * > '{"command":"progress","data":{"percentage":50}}' - */ -exports.buildMessage = (title, data = {}) => { - if (!_.isPlainObject(data)) { - throw errors.createError({ - title: `Invalid data: ${data}` - }) - } - - return JSON.stringify({ - command: title, - data - }) -} - -/** - * @summary Check whether a string is a robot message - * @function - * @public - * - * @description - * Note that this function doesn't check if the robot message - * is valid, but just that it is a robot message that we should - * attempt to parse. - * - * @param {String} string - string - * @returns {Boolean} whether the string is a robot message - * - * @example - * if (robot.isMessage(robot.buildMessage('foo', { - * message: 'bar' - * }))) { - * console.log('This is a robot message'); - * } - */ -exports.isMessage = (string) => { - try { - return _.isPlainObject(JSON.parse(string)) - } catch (error) { - return false - } -} - -/** - * @summary Parse a machine-parseable message - * @function - * @public - * - * @param {String} string - message string - * @returns {Object} parsed message - * - * @example - * const result = robot.parseMessage('{"command":"progress","data":{"foo":50}}'); - * console.log(result); - * > { - * > command: 'progress', - * > data: { - * > foo: 50 - * > } - * > } - */ -exports.parseMessage = (string) => { - let output = null - - try { - output = JSON.parse(string) - } catch (error) { - throw errors.createError({ - title: 'Invalid message', - description: `${string}, ${error.message}` - }) - } - - if (!output.command || !output.data) { - throw errors.createError({ - title: 'Invalid message', - description: `No command or data: ${string}` - }) - } - - return output -} - -/** - * @summary Build a machine-parseable error message - * @function - * @private - * - * @param {Error} error - error - * @returns {String} parseable error message - * - * @example - * const error = new Error('foo'); - * const errorMessage = robot.buildErrorMessage(error); - * - * console.log(error.command); - * > 'error' - * - * console.log(error.data.message); - * > 'foo' - */ -exports.buildErrorMessage = (error) => { - return exports.buildMessage(exports.COMMAND.ERROR, errors.toJSON(error)) -} - -/** - * @summary Recompose an error message - * @function - * @public - * - * @param {String} message - error message - * @returns {Error} error object - * - * @example - * const message = robot.buildErrorMessage(new Error('foo')); - * const error = robot.recomposeErrorMessage(robot.parseMessage(message)); - * - * error instanceof Error; - * > true - * - * console.log(error.message); - * > 'foo' - */ -exports.recomposeErrorMessage = (message) => { - return errors.fromJSON(message.data) -} - -/** - * @summary Get message command - * @function - * @public - * - * @param {Object} message - message - * @returns {String} command - * - * @example - * const command = robot.getCommand({ - * command: 'foo', - * data: {} - * }); - * - * console.log(command); - * > 'foo' - */ -exports.getCommand = (message) => { - return _.get(message, [ 'command' ]) -} - -/** - * @summary Get message data - * @function - * @public - * - * @param {Object} message - message - * @returns {Object} data - * - * @example - * const data = robot.getData({ - * command: 'foo', - * data: { - * foo: 1 - * } - * }); - * - * console.log(data); - * > { foo: 1 } - */ -exports.getData = (message) => { - return _.get(message, [ 'data' ], {}) -} - -/** - * @summary Print an error in a machine-friendly way - * @function - * @public - * - * @param {Error} error - error - * - * @example - * robot.printError(new Error('This is an error')); - */ -exports.printError = (error) => { - console.error(exports.buildErrorMessage(error)) -} - -/** - * @summary Print a message in a machine-friendly way - * @function - * @public - * - * @param {String} message - message - * @param {Object} [data] - data - * - * @example - * robot.printMessage('progress', { percentage: 50 }); - */ -exports.printMessage = (message, data) => { - console.log(exports.buildMessage(message, data)) -} - -/** - * @summary Log a message to the host's console - * @function - * @public - * - * @param {Object} [data] - data - * - * @example - * robot.log({ example: 'data' }); - */ -exports.log = (data) => { - exports.printMessage(exports.COMMAND.LOG, data) -} diff --git a/tests/shared/robot.spec.js b/tests/shared/robot.spec.js deleted file mode 100644 index 7906d0eb..00000000 --- a/tests/shared/robot.spec.js +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright 2016 resin.io - * - * 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' - -const m = require('mochainon') -const _ = require('lodash') -const robot = require('../../lib/shared/robot') - -describe('Shared: Robot', function () { - describe('.COMMAND', function () { - it('should be a plain object', function () { - m.chai.expect(_.isPlainObject(robot.COMMAND)).to.be.true - }) - - it('should only contain string values', function () { - m.chai.expect(_.every(_.values(robot.COMMAND), _.isString)).to.be.true - }) - - it('should contain only unique values', function () { - const numberOfKeys = _.size(_.keys(robot.COMMAND)) - m.chai.expect(_.size(_.uniq(_.values(robot.COMMAND)))).to.equal(numberOfKeys) - }) - }) - - describe('.isEnabled()', function () { - it('should return false if ETCHER_CLI_ROBOT is not set', function () { - m.chai.expect(robot.isEnabled({})).to.be.false - }) - - it('should return true if ETCHER_CLI_ROBOT=1', function () { - m.chai.expect(robot.isEnabled({ - ETCHER_CLI_ROBOT: 1 - })).to.be.true - }) - - it('should return false if ETCHER_CLI_ROBOT=0', function () { - m.chai.expect(robot.isEnabled({ - ETCHER_CLI_ROBOT: 0 - })).to.be.false - }) - - it('should return true if ETCHER_CLI_ROBOT="true"', function () { - m.chai.expect(robot.isEnabled({ - ETCHER_CLI_ROBOT: 'true' - })).to.be.true - }) - - it('should return false if ETCHER_CLI_ROBOT="false"', function () { - m.chai.expect(robot.isEnabled({ - ETCHER_CLI_ROBOT: 'false' - })).to.be.false - }) - - it('should return true if ETCHER_CLI_ROBOT=true', function () { - m.chai.expect(robot.isEnabled({ - ETCHER_CLI_ROBOT: true - })).to.be.true - }) - - it('should return false if ETCHER_CLI_ROBOT=false', function () { - m.chai.expect(robot.isEnabled({ - ETCHER_CLI_ROBOT: false - })).to.be.false - }) - }) - - describe('.buildMessage()', function () { - it('should build a message without data', function () { - const message = robot.buildMessage('hello') - const result = '{"command":"hello","data":{}}' - m.chai.expect(message).to.equal(result) - }) - - it('should build a message with data', function () { - const message = robot.buildMessage('hello', { - foo: 1, - bar: 2 - }) - const result = '{"command":"hello","data":{"foo":1,"bar":2}}' - m.chai.expect(message).to.equal(result) - }) - - it('should throw if data is defined but it not an object', function () { - m.chai.expect(() => { - robot.buildMessage('hello', 'world') - }).to.throw('Invalid data: world') - }) - }) - - describe('.isMessage()', function () { - it('should return true if message is an empty object', function () { - m.chai.expect(robot.isMessage('{}')).to.be.true - }) - - it('should return true if message is an object', function () { - m.chai.expect(robot.isMessage('{"command":"foo"}')).to.be.true - }) - - it('should return false if message is an invalid object', function () { - m.chai.expect(robot.isMessage('{"command":\\foo"}')).to.be.false - }) - - it('should return false if message is an unquoted string', function () { - m.chai.expect(robot.isMessage('foo')).to.be.false - }) - - it('should return false if message is an quoted string', function () { - m.chai.expect(robot.isMessage('"foo"')).to.be.false - }) - - it('should return false if message is an empty string', function () { - m.chai.expect(robot.isMessage('')).to.be.false - }) - - it('should return false if message is undefined', function () { - m.chai.expect(robot.isMessage(undefined)).to.be.false - }) - - it('should return false if message is null', function () { - m.chai.expect(robot.isMessage(null)).to.be.false - }) - - it('should return false if message is a positive integer string', function () { - m.chai.expect(robot.isMessage('5')).to.be.false - }) - - it('should return false if message is a negative integer string', function () { - m.chai.expect(robot.isMessage('-3')).to.be.false - }) - - it('should return false if message is a zero string', function () { - m.chai.expect(robot.isMessage('0')).to.be.false - }) - - it('should return false if message is a positive float string', function () { - m.chai.expect(robot.isMessage('5.3')).to.be.false - }) - - it('should return false if message is a negative float string', function () { - m.chai.expect(robot.isMessage('-2.1')).to.be.false - }) - - it('should return false if message is a positive integer', function () { - m.chai.expect(robot.isMessage(5)).to.be.false - }) - - it('should return false if message is a negative integer', function () { - m.chai.expect(robot.isMessage(-3)).to.be.false - }) - - it('should return false if message is zero', function () { - m.chai.expect(robot.isMessage(0)).to.be.false - }) - - it('should return false if message is a positive float', function () { - m.chai.expect(robot.isMessage(5.3)).to.be.false - }) - - it('should return false if message is a negative float', function () { - m.chai.expect(robot.isMessage(-2.1)).to.be.false - }) - - it('should return false if message is an array', function () { - m.chai.expect(robot.isMessage([ 'foo' ])).to.be.false - }) - - it('should return false if message is an array string', function () { - m.chai.expect(robot.isMessage('["foo"]')).to.be.false - }) - - it('should return true for a message built with .buildMessage()', function () { - m.chai.expect(robot.isMessage(robot.buildMessage('foo', { - message: 'bar' - }))).to.be.true - }) - - it('should return true for a message built with .buildErrorMessage()', function () { - const error = new Error('foo') - m.chai.expect(robot.isMessage(robot.buildErrorMessage(error))).to.be.true - }) - }) - - describe('.buildErrorMessage()', function () { - it('should build a message from a simple error', function () { - const error = new Error('foo') - const message = robot.buildErrorMessage(error) - - m.chai.expect(JSON.parse(message)).to.deep.equal({ - command: robot.COMMAND.ERROR, - data: { - message: 'foo', - stack: error.stack - } - }) - }) - - it('should save the error description', function () { - const error = new Error('foo') - error.description = 'error description' - const message = robot.buildErrorMessage(error) - - m.chai.expect(JSON.parse(message)).to.deep.equal({ - command: robot.COMMAND.ERROR, - data: { - message: 'foo', - description: 'error description', - stack: error.stack - } - }) - }) - - it('should save the error code', function () { - const error = new Error('foo') - error.code = 'MYERROR' - const message = robot.buildErrorMessage(error) - - m.chai.expect(JSON.parse(message)).to.deep.equal({ - command: robot.COMMAND.ERROR, - data: { - message: 'foo', - code: 'MYERROR', - stack: error.stack - } - }) - }) - - it('should handle a string error', function () { - const message = JSON.parse(robot.buildErrorMessage('foo')) - m.chai.expect(message.data.message).to.equal('foo') - m.chai.expect(message.data.stack).to.be.a.string - m.chai.expect(_.isEmpty(message.data.stack)).to.be.false - }) - }) - - describe('.parseMessage()', function () { - it('should parse a valid message', function () { - const message = robot.buildMessage('foo', { - bar: 1 - }) - - m.chai.expect(robot.parseMessage(message)).to.deep.equal({ - command: 'foo', - data: { - bar: 1 - } - }) - }) - - it('should parse a valid without data', function () { - const message = robot.buildMessage('foo') - m.chai.expect(robot.parseMessage(message)).to.deep.equal({ - command: 'foo', - data: {} - }) - }) - - it('should throw if input is not valid JSON', function () { - m.chai.expect(() => { - robot.parseMessage('Hello world\nFoo Bar') - }).to.throw('Invalid message') - }) - - it('should throw if input has no command', function () { - m.chai.expect(() => { - robot.parseMessage('{"data":{"foo":"bar"}}') - }).to.throw('Invalid message') - }) - - it('should throw if input has no data', function () { - m.chai.expect(() => { - robot.parseMessage('{"command":"foo"}') - }).to.throw('Invalid message') - }) - }) - - describe('.getCommand()', function () { - it('should get the command of a message', function () { - const message = robot.parseMessage(robot.buildMessage('hello', { - foo: 1, - bar: 2 - })) - - m.chai.expect(robot.getCommand(message)).to.equal('hello') - }) - }) - - describe('.getData()', function () { - it('should get the data of a message', function () { - const message = robot.parseMessage(robot.buildMessage('hello', { - foo: 1, - bar: 2 - })) - - m.chai.expect(robot.getData(message)).to.deep.equal({ - foo: 1, - bar: 2 - }) - }) - - it('should return an empty object if the message has no data', function () { - m.chai.expect(robot.getData({ - command: 'foo' - })).to.deep.equal({}) - }) - }) - - describe('.recomposeErrorMessage()', function () { - it('should return an instance of Error', function () { - const error = new Error('Foo bar') - const message = robot.parseMessage(robot.buildErrorMessage(error)) - m.chai.expect(robot.recomposeErrorMessage(message)).to.be.an.instanceof(Error) - }) - - it('should be able to recompose an error object', function () { - const error = new Error('Foo bar') - const message = robot.parseMessage(robot.buildErrorMessage(error)) - m.chai.expect(robot.recomposeErrorMessage(message)).to.deep.equal(error) - }) - - it('should be able to recompose an error object with a code', function () { - const error = new Error('Foo bar') - error.code = 'FOO' - const message = robot.parseMessage(robot.buildErrorMessage(error)) - m.chai.expect(robot.recomposeErrorMessage(message)).to.deep.equal(error) - }) - - it('should be able to recompose an error object with a description', function () { - const error = new Error('Foo bar') - error.description = 'My description' - const message = robot.parseMessage(robot.buildErrorMessage(error)) - m.chai.expect(robot.recomposeErrorMessage(message)).to.deep.equal(error) - }) - }) -})