Merge pull request #2029 from resin-io/remove-unused-robot

refactor(lib): Remove unused robot protocol
This commit is contained in:
Jonas Hermsmeier 2018-02-12 04:58:52 -08:00 committed by GitHub
commit 2e1764af82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 12 additions and 794 deletions

View File

@ -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
-------

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
})
})
})