Added deepforge management cli. Fixes #526 (#545)

WIP #526 Added update command

WIP #526 Added start command

WIP #488 Added mongo detection

WIP #526 Added torch checking & installation

WIP #526 Added help if no args and 'mongodb not installed' msg

WIP #526 Made mongo check into a promise

WIP #526 Made mongodb dir customizable

WIP #526 Config getting/setting

WIP #526 Fixed mongodb starting/checking

WIP #526 Added --torch to update cmd

WIP #526 Added uninstall command

WIP #526 Added --server opt to update and updated package.json

WIP #526 Added eslintrc for bin scripts

WIP #526 minor description fix

WIP #526 Fixed missing dependency

WIP #526 Added some cli test boilerplate

WIP #526 Added cli tests
Esse commit está contido em:
Brian Broll
2016-07-25 12:03:31 -05:00
commit de GitHub
commit 0e6ae8f9b6
5 arquivos alterados com 624 adições e 2 exclusões
+21
Ver Arquivo
@@ -0,0 +1,21 @@
env:
browser: true
node: true
mocha: true
es6: true
extends: 'eslint:recommended'
rules:
no-console:
- 0
indent:
- 2
- 4
linebreak-style:
- 2
- unix
quotes:
- 2
- single
semi:
- 2
- always
+8
Ver Arquivo
@@ -0,0 +1,8 @@
{
"torch": {
"dir": "~/.deepforge/torch"
},
"mongo": {
"dir": "~/.deepforge/data"
}
}
Arquivo executável
+393
Ver Arquivo
@@ -0,0 +1,393 @@
#!/usr/bin/env node
var Command = require('commander').Command,
program = new Command(),
childProcess = require('child_process'),
spawn = childProcess.spawn,
execSync = childProcess.execSync,
path = require('path'),
fs = require('fs'),
version = require('../package.json').version,
exists = require('exists-file'),
forever = require('forever-monitor'),
DEFAULT_CONFIG = require('./config.json'),
assign = require('lodash.assign'),
config,
configDir = path.join(process.env.HOME, '.deepforge'),
configPath = path.join(configDir, 'config.json'),
dataPath = path.join(configDir, 'data'),
localConfig,
p = dir => dir.replace(/^~/, process.env.HOME); // resolve '~' to '$HOME'
// Check for any commands
if (process.argv.length === 2) {
process.argv.push('--help');
}
// Create the config if it doesn't exist
if (!exists.sync(configDir)) {
fs.mkdirSync(configDir);
}
if (!exists.sync(dataPath)) {
fs.mkdirSync(dataPath);
}
if (!exists.sync(configPath)) {
fs.writeFileSync(configPath, '{\n}');
}
localConfig = require(configPath);
config = assign(DEFAULT_CONFIG, require(configPath));
var getConfigValue = function(id) {
var keys = id.split('.'),
value = config;
for (var i = 0; i < keys.length; i++) {
value = value[keys[i]];
if (!value) {
return null;
}
}
return value;
};
var storeConfig = function(id, value) {
// load the config
var keys = id.split('.').filter(k => k),
lastKey = keys.pop(),
currentObj = localConfig,
current = getConfigValue(id);
// Check if it is a valid key
if (current === null) {
return false;
}
for (var i = 0; i < keys.length; i++) {
if (!currentObj[keys[i]]) {
currentObj[keys[i]] = {};
}
currentObj = currentObj[keys[i]];
}
currentObj[lastKey] = value;
fs.writeFileSync(configPath, JSON.stringify(localConfig, null, 2));
return true;
};
program
.version('v' + version)
.description('Command line interface for managing deepforge');
// start
var start = function(main, opts) {
var child = new forever.Monitor(main, opts);
child.on('exit', function () {
console.log('Exited after 3 failed restarts');
});
child.start();
};
var isLocalUri = function(protocol, uri) {
return uri.indexOf(protocol + '://localhost') === 0 ||
uri.indexOf(protocol + '://127.0.0.1') === 0;
};
var checkMongo = function(args) {
// check the webgme config
var gmeConfig = require('../config'),
mongoUri = gmeConfig.mongo.uri;
if (isLocalUri('mongodb', mongoUri)) {
// Make sure mongo is running locally (using pgrep)
try {
execSync('pgrep mongod').toString();
console.log('MongoDB is already running!');
} catch (e) { // no pIds
console.log('Starting MongoDB...');
startMongo(args, true);
}
}
};
var startMongo = function(args, silent) {
var job = spawn('mongod', ['--dbpath', p(config.mongo.dir)], {
cwd: process.env.HOME
});
if (!silent) {
job.stdout.on('data',
data => process.stdout.write(data.toString()));
}
job.on('error', err => {
if (err.code === 'ENOENT') {
console.log('Could not find MongoDB. Is it installed?');
if (!args.mongo) {
console.log('Otherwise, set MONGO_URI to the desired mongo uri and try again:');
console.log('');
console.log(' MONGO_URI=mongodb://some.other.ip:27017' +
`/deepforge deepforge ${process.argv.slice(2).join(' ')}`);
console.log('');
}
} else {
console.log('Error encountered while starting MongoDB');
throw err;
}
});
job.stderr.on('data', data => {
var msg = 'mongodb: ' + data;
process.stdout.write(msg);
});
job.on('exit', code => {
if (code) {
console.log('MongoDB closed w/ error code: ' + code);
}
});
};
var checkTorch = function() {
return new Promise(_checkTorch)
.catch(() => 'Torch installation failed');
};
var _checkTorch = function(resolve, reject) {
var result = childProcess.spawnSync('th', ['--help']),
tgtDir = p(config.torch.dir),
cmds;
if (result.error) {
// Try to install torch
console.log(`Torch7 not found. Installing to ${tgtDir}...`);
cmds = [
`git clone https://github.com/torch/distro.git ${tgtDir} --recursive`,
`cd ${tgtDir}`,
'bash install-deps',
'./install.sh'
];
spawnMany(cmds,
() => {
storeConfig('torch.dir', tgtDir);
resolve(true);
},
reject
);
} else {
resolve(false);
}
};
var spawnMany = function(cmds, succ, err) {
var rawCmd,
cmd,
args,
job;
if (cmds.length === 0) {
return succ();
}
rawCmd = cmds.shift();
args = rawCmd.split(' ');
cmd = args.shift();
job = spawn(cmd, args);
job.stdout.on('data', data => process.stdout.write(data));
job.stderr.on('data', data => process.stderr.write(data));
job.on('close', code => {
if (code) {
console.log(`${rawCmd} failed w/ error code ${code}`);
err(code, rawCmd);
} else {
spawnMany(cmds, succ, err);
}
});
};
program.command('start')
.description('start deepforge locally (default) or specific components')
.option('-p, --port <port>', 'specify the port to use')
.option('-s, --server', 'start the server')
.option('-w, --worker [url]', 'start a worker and connect to given url. Defaults to local deepforge')
.option('-m, --mongo', 'start MongoDB')
.action(args => {
var main = path.join(__dirname, 'start-local.js'),
opts;
opts = {
max: 3,
args: []
};
if (args.port) {
opts.env = {
PORT: args.port
};
}
if (args.server) {
checkMongo(args);
main = path.join(__dirname, '..', 'app.js');
start(main, opts);
}
if (args.worker) {
checkTorch().then(() => {
main = path.join(__dirname, 'start-worker.js');
if (args.worker !== true) {
opts.args.push(args.worker);
}
start(main, opts);
});
}
if (args.mongo) {
startMongo(args);
}
if (!args.server && !args.worker && !args.mongo) {
// Starting everything
checkMongo(args);
checkTorch().then(() => start(main, opts));
}
});
// update
program
.command('update')
.description('upgrade deepforge to latest version')
.option('-g, --git', 'update tracking the git repo')
.option('-t, --torch', 'update torch installation')
.option('-s, --server', 'update deepforge')
.action(args => {
var pkg = 'deepforge',
job,
latestVersion;
// Install the project
if (!args.torch || args.server) {
if (args.git) {
pkg = 'dfst/deepforge';
} else {
// Check the version
try {
latestVersion = execSync('npm show deepforge version')
.toString().replace(/\s+$/, '');
if (latestVersion === version) {
console.log('Already up-to-date');
return;
}
} catch (e) {
console.log('Could not retrieve the latest deepforge version');
}
}
job = spawn('npm', ['install', '-g', pkg]);
job.stdout.on('data', data => process.stdout.write(data.toString()));
job.stderr.on('data', data => process.stderr.write(data.toString()));
job.on('close', code => {
if (!code) {
console.log('Upgrade successful!');
} else {
console.log('Upgrade failed w/ error code: ' + code);
}
});
}
if (args.torch || !args.server) {
// Update torch
checkTorch().then(justInstalled => {
if (!justInstalled) {
// Upgrade torch
console.log('Upgrading torch...');
job = spawn('bash', ['./update.sh'], {
cwd: p(config.torch.dir)
});
job.stdout.on('data', data => process.stdout.write(data.toString()));
job.stderr.on('data', data => process.stderr.write(data.toString()));
job.on('close', code => {
if (!code) {
console.log('Upgrade successful!');
} else {
console.log('Upgrade failed w/ error code: ' + code);
}
});
}
});
}
});
// uninstall command
program
.command('uninstall')
.description('uninstall deepforge from the system')
.option('-t, --torch', 'uninstall torch')
.option('-c, --clean', 'uninstall deepforge, torch and all associated data/config')
.action(opts => {
if (opts.torch || opts.clean) {
if (opts.torch) {
console.log(`uninstalling torch at ${p(config.torch.dir)}`);
}
fs.unlinkSync(p(config.torch.dir));
}
if (opts.clean) { // remove the .deepforge directory
console.log('removing config and data files...');
fs.unlinkSync(p(config.mongo.dir));
fs.unlinkSync(p(configDir));
}
if (!opts.torch || opts.clean) { // uninstall deepforge
spawnMany(
['npm uninstall -g deepforge'],
() => console.log('deepforge has been uninstalled!'),
() => console.log('uninstall failed')
);
}
});
// config
program
.command('config [key] [value]')
.description('read or edit config options (omit "value" to see current value)')
.action(key => {
var value = program.args[1],
success;
if (value) { // write a value
success = storeConfig(key, value);
if (success) {
console.log('Config has been updated!');
}
} else if (key) { // read a single value
value = getConfigValue(key);
if (value === null) {
console.log(`Invalid config value: "${key}"`);
return;
}
if (typeof value === 'object') {
value = JSON.stringify(value, null, 2);
}
console.log(value);
} else { // print entire config
console.log(`Current config:\n${JSON.stringify(config, null, 2)}`);
}
});
module.exports = function(cmd) {
var cmds = cmd.split(/\s+/).filter(w => !!w);
cmds.unshift('node');
cmds.unshift('./bin/deepforge');
program.parse(cmds);
};
if (require.main === module) {
program.parse(process.argv);
}
+10 -2
Ver Arquivo
@@ -1,5 +1,8 @@
{
"name": "deepforge",
"bin": {
"deepforge": "./bin/deepforge"
},
"scripts": {
"start": "node app.js",
"start-dev": "NODE_ENV=dev node app.js",
@@ -11,7 +14,11 @@
},
"version": "0.9.0",
"dependencies": {
"commander": "^2.9.0",
"dotenv": "^2.0.0",
"exists-file": "^2.1.0",
"forever-monitor": "^1.7.0",
"lodash.assign": "^4.0.9",
"lodash.difference": "^4.1.2",
"nodemon": "^1.9.2",
"webgme": "^2.0.0",
@@ -23,9 +30,10 @@
"webgme-simple-nodes": "^2.0.0"
},
"devDependencies": {
"chai": "^3.0.0",
"jszip": "^2.5.0",
"mocha": "^2.2.5",
"rimraf": "^2.4.0",
"chai": "^3.0.0"
"mockery": "^1.7.0",
"rimraf": "^2.4.0"
}
}
+192
Ver Arquivo
@@ -0,0 +1,192 @@
var mockery = require('mockery'),
fs = require('fs'),
assert = require('assert'),
path = require('path'),
nop = () => {},
cli;
var callRegister = {
childProcess: {
execSync: []
}
};
var mocks = {
childProcess: {},
forever: {}
};
var childProcess = {
execSync: function(cmd) {
callRegister.childProcess.execSync.push(cmd);
if (mocks.childProcess.execSync) {
return mocks.childProcess.execSync.apply(this, arguments);
}
},
spawn: function(cmd) {
if (mocks.childProcess.spawn) {
mocks.childProcess.spawn.apply(this, arguments);
}
return {
on: () => {},
stdout: {
on: () => {}
},
stderr: {
on: () => {}
}
};
}
};
var forever = {};
forever.Monitor = function() {
var res = {};
res.on = nop;
res.start = nop;
if (mocks.forever.Monitor) {
mocks.forever.Monitor.apply(this, arguments);
}
return res;
};
describe('cli', function() {
before(function() {
// create the mocks
mockery.enable({
warnOnReplace: false,
warnOnUnregistered: false
});
mockery.registerMock('child_process', childProcess);
mockery.registerMock('forever-monitor', forever);
cli = require('../../bin/deepforge');
});
it('should display help message if no args', function() {
// TODO
});
describe('start', function() {
afterEach(function() {
callRegister.childProcess.execSync = [];
mocks.childProcess.execSync = nop;
mocks.childProcess.spawn = nop;
mocks.forever.Monitor = nop;
});
it('should check for running mongo', function() {
var calls;
callRegister.childProcess.execSync = [];
cli('start');
calls = callRegister.childProcess.execSync;
assert.notEqual(calls.indexOf('pgrep mongod'), -1);
});
it('should start mongo if no running mongo', function() {
mocks.childProcess.execSync = (cmd) => {
if (cmd === 'pgrep mongod') {
throw 'No pIds';
}
};
// Check that mongo is started
mocks.childProcess.spawn = cmd => {
assert.equal(cmd, 'mongod');
};
cli('start');
});
it('should start mongo w/ dbpath', function() {
// Check that mongo is started
mocks.childProcess.spawn = (cmd, args) => {
assert.equal(cmd, 'mongod');
assert.equal(args[0], '--dbpath');
assert.equal(args.length, 2);
};
cli('start --mongo');
});
it('should start local deepforge by default', function() {
mocks.forever.Monitor = main =>
assert.notEqual(main.indexOf('start-local.js'), -1);
cli('start');
});
it('should start normal deepforge if --server set', function() {
mocks.forever.Monitor = main =>
assert.notEqual(main.indexOf('app.js'), -1);
cli('start --server');
});
it('should start worker if --worker set', function(done) {
mocks.forever.Monitor = main => {
if (main.indexOf('start-worker.js') !== -1) {
done();
}
};
cli('start --worker');
});
});
describe('uninstall', function() {
it('should only remove \'torch\' if --torch option set', function() {
var oldUnlink = fs.unlinkSync;
fs.unlinkSync = path => assert.notEqual(path.indexOf('torch'), -1);
cli('uninstall --torch');
fs.unlinkSync = oldUnlink;
});
it('should uninstall deepforge w/ npm', function() {
mocks.childProcess.spawn = (cmd, args) => {
assert.equal(cmd, 'npm');
assert.equal(args[0], 'uninstall');
assert.notEqual(args.indexOf('deepforge'), -1);
};
var oldUnlink = fs.unlinkSync;
fs.unlinkSync = nop;
cli('uninstall');
fs.unlinkSync = oldUnlink;
});
it('should remove ~/.deepforge if --clean option set', function(done) {
var oldUnlink = fs.unlinkSync;
fs.unlinkSync = dir => {
if (dir === path.join(process.env.HOME, '.deepforge')) {
done();
}
};
cli('uninstall --clean');
fs.unlinkSync = oldUnlink;
});
});
describe('update', function() {
it('should update deepforge w/ npm', function() {
mocks.childProcess.spawn = (cmd, args) => {
assert.equal(cmd, 'npm');
assert.equal(args[0], 'install');
assert.notEqual(args.indexOf('deepforge'), -1);
assert.notEqual(args.indexOf('-g'), -1);
};
cli('update');
});
it('should update deepforge from git if --git set w/ npm', function() {
mocks.childProcess.spawn = (cmd, args) => {
assert.notEqual(args.indexOf('dfst/deepforge'), -1);
};
cli('update --git');
});
it('should update torch if --torch', function() {
mocks.childProcess.spawn = (cmd, args) => {
assert.equal(cmd, 'bash');
assert.equal(args[0], './update.sh');
};
cli('update --torch');
});
});
after(function() {
mockery.disable();
});
});