Merge pull request #5 from Tjatse/development

push
This commit is contained in:
Jun 2014-12-31 18:22:15 +08:00
commit 885cfa27fc
22 changed files with 423 additions and 88 deletions

1
.gitignore vendored
View File

@ -1,2 +1 @@
test
node_modules

View File

@ -1,4 +1,3 @@
node_modules
.gitignore
test
screenshots

View File

@ -1,9 +1,9 @@
pm2-gui [![NPM version](https://badge.fury.io/js/pm2-gui.svg)](http://badge.fury.io/js/pm2-gui)
pm2-gui [![NPM version](https://badge.fury.io/js/pm2-gui.svg)](http://badge.fury.io/js/pm2-gui) [![Build Status](https://travis-ci.org/Tjatse/pm2-gui.svg?branch=master)](https://travis-ci.org/Tjatse/pm2-gui)
=======
An elegant web interface for Unitech/PM2.
> Compatible with PM2 v0.12.2.
> Compatible with PM2 v0.12.3.
# Guide
- [Features](#feats)
@ -14,7 +14,6 @@ An elegant web interface for Unitech/PM2.
- [Configs](#cli_confs)
- [Authorization](#auth)
- [UI/UX](#ui)
- [TODO](#todo)
<a name="feats" />
# Feature
@ -74,24 +73,40 @@ $ npm install -g pm2-gui
Options:
-h, --help output usage information
-h, --help output usage information
--config [file] pass JSON config file with options
--no-debug hide stdout/stderr information
--config path to custom .json config. Default value pm2-gui.json
```
<a name="cli_confs" />
## Configs
```javascript
{
"refresh": 3000
"manipulation": true
"pm2": "~/.pm2"
"refresh": 3000,
"manipulation": true,
"pm2": "~/.pm2",
"port": 8088
}
```
- **refresh** The heartbeat duration of monitor (backend), `5000` by default.
- **manupulation** A value indicates whether the client has permission to restart/stop processes, `true` by default.
- **manipulation** A value indicates whether the client has permission to restart/stop processes, `true` by default.
- **pm2** Root directory of Unitech/PM2, `~/.pm2` by default.
- **password** The encrypted authentication code, if this config is set, users need to be authorized before accessing the index page.
- **password** The encrypted authentication code, if this config is set, users need to be authorized before accessing the index page, `password` could only be set by `pm2-gui set password [password]`.
- **port** Web GUI port, can be set only from config file
### Config file
You can quit set configurations by `pm2-gui start --config [file]`, the `[file]` must be an valid JSON, and can including all the above keys.
Example
```bash
# Load the JSON configured file which is named as `pm2-gui.json` in current directory.
$ pm2-gui start --config
# Load the specific JSON configured file in current directory.
$ pm2-gui start --config conf.json
```
### Set Config
Usage
@ -168,15 +183,6 @@ Tail Logs
![image](screenshots/tail-logs.jpg)
<a name="todo" />
# TODO
- [x] Authentication
- [ ] Multiple operations.
- [ ] Configured JSON files.
- [ ] Memory and CPU usage gauge of each process.
- [ ] Test on Internet Explorer (need environment && PRs).
- [ ] Need feedback/test.
## License
Licensed under the Apache License, Version 2.0 (the "License");

View File

@ -2,6 +2,7 @@
var commander = require('commander'),
path = p = require('path'),
fs = require('fs'),
chalk = require('chalk'),
_ = require('lodash'),
pkg = require('../package.json'),
@ -21,31 +22,38 @@ commander.on('--help', function(){
chalk.grey(' $ pm2-gui start 8090\n')
);
});
commander.on('-c', function(){
console.log(arguments);
})
// Web interface
/**
* Run web interface.
*/
commander.command('start [port]')
.option('--no-debug', 'hide stdout/stderr information')
.option('--config [file]', 'pass JSON config file with options')
.option('--no-debug', 'hide stdout / stderr information')
.description('Launch the web server, port default by 8088')
.action(function(port, cmd){
interface(port, cmd.debug);
});
if (cmd.config) {
var jsonFile;
if (typeof cmd.config != 'string') {
jsonFile = 'pm2-gui.json';
} else {
jsonFile = cmd.config;
}
if (!fs.existsSync(jsonFile)) {
console.log(chalk.red('✘ JSON configured file does not exist!\n'));
process.exit();
}
// Configuration
var acceptKeys = ['pm2', 'refresh', 'manipulation', 'password'];
function showConfigs(cmd, mon){
if (!mon) {
mon = Monitor();
}
var storage = mon._config.store, prints = '';
for (var k in storage) {
prints += Array(15 - k.length).join(' ') + chalk.bold(k + ': ') + ' ' + chalk.blue(storage[k] + '\n');
}
console.log(prints);
}
try {
var config = JSON.parse(fs.readFileSync(jsonFile, {encoding: 'utf-8'}));
setConfig(config);
} catch (err) {
console.log(chalk.red('✘ JSON configured file is invalid!\n'));
process.exit();
}
}
port && setConfig('port', port);
interface(cmd.debug);
});
commander.command('config')
.description('show all configs')
@ -54,19 +62,8 @@ commander.command('config')
commander.command('set <key> <value>')
.description('set config by key-value pairs')
.action(function(key, value, cmd){
if (!~acceptKeys.indexOf(key)) {
return console.log('key could only be one of below:', acceptKeys.map(function(m){
return '\n' + chalk.magenta(m)
}).join(''));
}
var mon = Monitor();
if (key == acceptKeys[acceptKeys.length - 1]) {
var md5 = crypto.createHash('md5');
md5.update(value);
value = md5.digest('hex');
}
mon.config(key, value);
showConfigs(cmd, mon);
var mon = setConfig(key, value);
mon && showConfigs(cmd, mon);
});
commander.command('rm <key>')
@ -82,4 +79,51 @@ commander.parse(process.argv);
if (process.argv.length == 2) {
commander.outputHelp();
process.exit(0);
}
/**
* Set configuration.
* @param key
* @param value
* @returns {*}
*/
function setConfig(key, value){
var mon = Monitor(),
acceptKeys = Object.keys(mon.options).filter(function(key){
return !~['socketio', 'pm2Conf', 'debug'].indexOf(key);
});
(function config(pairs){
if (pairs.length == 0) {
return;
}
var pair = pairs.shift();
if (!~acceptKeys.indexOf(pair[0])) {
return config(pairs);
}
if (pair[0] == 'password') {
var md5 = crypto.createHash('md5');
md5.update(pair[1]);
pair[1] = md5.digest('hex');
}
mon.config(pair[0], pair[1]);
config(pairs);
})(typeof key == 'object' ? _.pairs(key) : [[key, value]]);
return mon;
}
/**
* Show all configurations.
* @param cmd
* @param mon
*/
function showConfigs(cmd, mon){
if (!mon) {
mon = Monitor();
}
var storage = mon._config.store, prints = '';
for (var k in storage) {
prints += Array(15 - k.length).join(' ') + chalk.bold(k + ': ') + ' ' + chalk.blue(storage[k] + '\n');
}
console.log(prints);
}

View File

@ -8,7 +8,8 @@ var fs = require('fs'),
ansiHTML = require('ansi-html'),
pm = require('./pm'),
totalmem = require('os').totalmem(),
pidusage = require('pidusage');
pidusage = require('pidusage'),
defConf = require('../pm2-gui.conf');
module.exports = Monitor;
@ -33,30 +34,34 @@ function Monitor(options){
this._init(options);
};
Monitor.prototype._resolveHome = function(pm2Home){
if (pm2Home.indexOf('~/') == 0) {
// Get root directory of PM2.
pm2Home = process.env.PM2_HOME || p.resolve(process.env.HOME || process.env.HOMEPATH, pm2Home.substr(2));
// Make sure exist.
if (!pm2Home || !fs.existsSync(pm2Home)) {
throw new Error('PM2 root can not be located, try to set env by `export PM2_HOME=[ROOT]`.');
}
}
return pm2Home;
}
/**
* Initialize options and configurations.
* @private
*/
Monitor.prototype._init = function(options){
options = options || {};
// bind default options.
_.defaults(options, {
refresh : 5000,
manipulation: true
});
options = _.defaults(defConf, options);
// Get root directory of PM2.
var pm2Root = process.env.PM2_HOME || p.resolve(process.env.HOME || process.env.HOMEPATH, '.pm2');
// Make sure exist.
if (!pm2Root || !fs.existsSync(pm2Root)) {
throw new Error('PM2 root can not be located, try to set env by `export PM2_HOME=[ROOT]`.');
}
options.pm2 = this._resolveHome(options.pm2);
// Load PM2 config.
var pm2ConfPath = path.join(pm2Root, 'conf.js');
var pm2ConfPath = path.join(options.pm2, 'conf.js');
try {
options.pm2Conf = require(pm2ConfPath)(pm2Root);
options.pm2Conf = require(pm2ConfPath)(options.pm2);
if (!options.pm2Conf) {
throw new Error(404);
}
@ -64,8 +69,6 @@ Monitor.prototype._init = function(options){
throw new Error('Can not load PM2 config, the file "' + pm2ConfPath + '" does not exist.');
}
options.pm2Root = pm2Root;
// Bind socket.io server to context.
if (options.sockio) {
this._sockio = options.sockio;
@ -77,12 +80,18 @@ Monitor.prototype._init = function(options){
Object.freeze(this.options);
// Initialize configurations.
this._config = new nconf.File({file: path.resolve(this.options.pm2Root, 'pm2-gui.json')});
this._config = new nconf.File({file: path.resolve(this.options.pm2, 'pm2-gui.json')});
// Set configurations.
this.config('pm2', this._config.get('pm2') || this.options.pm2Root);
this.config('refresh', this._config.get('refresh') || this.options.refresh);
this.config('manipulation', this._config.get('manipulation') || this.options.manipulation || true);
this.config('pm2', this._resolveHome(this.config('pm2')) || this.options.pm2);
this.config('refresh', this.config('refresh') || this.options.refresh);
this.config('port', this.config('port') || this.options.port || 8088);
var mani = false;
if (typeof (mani = this.config('manipulation')) == 'undefined' && typeof (mani = this.options.manipulation) == 'undefined') {
mani = true;
}
this.config('manipulation', mani);
// Logger.
this._log = Debug({
@ -115,12 +124,8 @@ Monitor.prototype.config = function(key, value){
// Clear config.
this._config.clear(key);
// Reset to default if necessary.
if (key == 'refresh') {
value = 5000;
} else if (key == 'manipulation') {
value = true;
}
value && this._config.set(key, value);
var value = defConf[key];
(typeof value != 'undefined') && this._config.set(key, value);
return this._config.saveSync();
}
@ -132,6 +137,7 @@ Monitor.prototype.config = function(key, value){
value = (value == 'true');
}
}
this._config.set(key, value);
// Save it.
this._config.saveSync();
@ -350,8 +356,8 @@ Monitor.prototype._connectProcSock = function(socket){
stat.memory = stat.memory * 100 / totalmem;
// Emit memory/CPU usage to clients.
broadcast.call(ctx, {
pid : pid,
time: Date.now(),
pid : pid,
time : Date.now(),
usage: stat
});
});

View File

@ -3,6 +3,7 @@
"version": "0.0.7",
"description": "An elegant web interface for Unitech/PM2.",
"scripts": {
"test": "NODE_ENV=test bash test/index.sh"
},
"bin": {
"pm2-gui": "bin/pm2-gui"

6
pm2-gui.conf Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
"pm2": "~/.pm2",
"refresh": 5000,
"manipulation": true,
"port": 8088
};

43
test/bash/config.sh Normal file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env bash
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/include.sh"
cd $fixtures
head "set config (Number)(refresh)"
$pg set refresh 4000 > /dev/null
val=`$pg config | grep "refresh:" | egrep -oh "\d+"`
[ $val -eq 4000 ] || fail "expect the value to be 4000, but current is $val"
success "the value should be 4000"
head "set config (Number)(port)"
$pg set port 9000 > /dev/null
val=`$pg config | grep "port:" | egrep -oh "\d+"`
[ $val -eq 9000 ] || fail "expect the value to be 9000, but current is $val"
success "the value should be 9000"
head "set config (Boolean)"
$pg set manipulation false > /dev/null
val=`$pg config | grep "manipulation:" | egrep -oh "(true|false)"`
[ $val = false ] || fail "expect the value to be false, but current is $val"
success "the value should be false"
head "set config (String)"
tmpPM2="/tmp/.pm2"
if [ ! -d "$tmpPM2" ]; then
mkdir "$tmpPM2"
fi
$pg set pm2 "$tmpPM2" > /dev/null
val=`$pg config | grep "pm2:" | egrep -oh "$tmpPM2$" | wc -c`
[ $val -gt 0 ] || fail "expect the value to be /tmp/.pm2"
success "the value should be /tmp/.pm2"
$pg set pm2 "~/.pm2" > /dev/null
$pg set port 8088 > /dev/null

42
test/bash/include.sh Normal file
View File

@ -0,0 +1,42 @@
#!/usr/bin/env bash
node="`type -P node`"
nodeVersion="`$node -v`"
pg="`type -P node` `pwd`/bin/pm2-gui"
fixtures="test/fixtures"
function success {
echo -e "\033[32m ✔ $1\033[0m"
}
function fail {
echo -e "######## \033[31m ✘ $1\033[0m"
exit 1
}
function spec {
RET=$?
sleep 0.3
[ $RET -eq 0 ] || fail "$1"
success "$1"
}
function ispec {
RET=$?
sleep 0.3
[ $RET -ne 0 ] || fail "$1"
success "$1"
}
function should {
sleep 0.5
OUT=`$pm2 prettylist | grep -o "$2" | wc -l`
[ $OUT -eq $3 ] || fail "$1"
success "$1"
}
function head {
echo -e "\x1B[1;35m$1\x1B[0m"
}

114
test/bash/interface.sh Normal file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env bash
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/include.sh"
cd $fixtures
$pg set port 8088 > /dev/null
head "run web server (default port)"
nohup $pg start > /dev/null 2>&1 &
pid=$!
sleep 1
ret=`nc 127.0.0.1 8088 < /dev/null; echo $?`
[ $ret -eq 0 ] || fail "expect 127.0.0.1:8088 can be connected"
success "127.0.0.1:8088 should be connected"
kill "$pid"
sleep 1
ret=`nc 127.0.0.1 8088 < /dev/null; echo $?`
[ $ret -eq 1 ] || fail "expect 127.0.0.1:8088 can not be connected"
success "127.0.0.1:8088 should be disconnected"
head "run web server (customized port: 9000)"
nohup $pg start 9000 > /dev/null 2>&1 &
pid=$!
sleep 1
ret=`nc 127.0.0.1 9000 < /dev/null; echo $?`
[ $ret -eq 0 ] || fail "expect 127.0.0.1:9000 can be connected"
success "127.0.0.1:9000 should be connected"
kill "$pid"
sleep 1
ret=`nc 127.0.0.1 9000 < /dev/null; echo $?`
[ $ret -eq 1 ] || fail "expect 127.0.0.1:9000 can not be connected"
success "127.0.0.1:9000 should be disconnected"
head "run web server (--config verify)"
ret=`$pg start --config not_exist.json | grep "does not exist" | wc -c`
[ $ret -gt 0 ] || fail "expect throw out error message"
success "JSON file does not exist"
ret=`$pg start --config invalid.conf | grep "invalid" | wc -c`
[ $ret -gt 0 ] || fail "expect throw out error message"
success "JSON file invalid"
head "run web server (--config specific file)"
nohup $pg start --config pm2-gui-cp.conf > /dev/null 2>&1 &
pid=$!
sleep 1
ret=`nc 127.0.0.1 27130 < /dev/null; echo $?`
[ $ret -eq 0 ] || fail "expect 127.0.0.1:27130 can be connected"
success "127.0.0.1:27130 should be connected"
kill "$pid"
sleep 1
ret=`nc 127.0.0.1 27130 < /dev/null; echo $?`
[ $ret -eq 1 ] || fail "expect 127.0.0.1:27130 can not be connected"
success "127.0.0.1:27130 should be disconnected"
val=`$pg config | grep "refresh:" | egrep -oh "\d+"`
[ $val -eq 3000 ] || fail "expect the value of refresh to be 3000, but current is $val"
success "the value of refresh should be 3000"
val=`$pg config | grep "manipulation:" | egrep -oh "(true|false)"`
[ $val = false ] || fail "expect the value of manipulation to be false, but current is $val"
success "the value of manipulation should be false"
val=`$pg config | grep "pm2:" | egrep -oh "/tmp/\.pm2$" | wc -c`
[ $val -gt 0 ] || fail "expect the value of pm2 to be /tmp/.pm2"
success "the value of pm2 should be /tmp/.pm2"
head "run web server (--config default file)"
nohup $pg start --config > /dev/null 2>&1 &
pid=$!
sleep 1
ret=`nc 127.0.0.1 8088 < /dev/null; echo $?`
[ $ret -eq 0 ] || fail "expect 127.0.0.1:8088 can be connected"
success "127.0.0.1:8088 should be connected"
kill "$pid"
sleep 1
ret=`nc 127.0.0.1 8088 < /dev/null; echo $?`
[ $ret -eq 1 ] || fail "expect 127.0.0.1:8088 can not be connected"
success "127.0.0.1:8088 should be disconnected"
val=`$pg config | grep "refresh:" | egrep -oh "\d+"`
[ $val -eq 5000 ] || fail "expect the value of refresh to be 5000, but current is $val"
success "the value of refresh should be 3000"
val=`$pg config | grep "manipulation:" | egrep -oh "(true|false)"`
[ $val = true ] || fail "expect the value of manipulation to be true, but current is $val"
success "the value of manipulation should be true"
root="~/.pm2"
if [ -z "$PM2_HOME" ]
then
root="$PM2_HOME"
else
if [ -z "$HOME" ]
then
root="$HOME/.pm2"
else
if [ -z "$HOMEPATH" ]
then
root="$HOMEPATH/.pm2"
fi
fi
fi
val=`$pg config | grep "pm2:" | egrep -oh "$root$" | wc -c`
[ $val -gt 0 ] || fail "expect the value of pm2 to be $root"
success "the value of pm2 should be $root"
$pg set port 8088 > /dev/null

14
test/fixtures/colorful.js vendored Normal file
View File

@ -0,0 +1,14 @@
var chalk = require('chalk');
console.log('This is', chalk.bold.red('red'));
console.log('This is', chalk.dim.green('green'));
console.log('This is', chalk.bold.green('green'));
console.log('This is', chalk.bold.italic.yellow('yellow'));
console.log('This is', chalk.bold.strikethrough.blue('blue'));
console.log('This is', chalk.bold.underline.magenta('magenta'));
console.log('This is', chalk.bold.cyan('cyan'));
console.log('This is', chalk.bold.grey('grey'));
setTimeout(function(){
}, 3000000);

1
test/fixtures/exit.js vendored Normal file
View File

@ -0,0 +1 @@
console.log('ok at', Date.now());

13
test/fixtures/fib-slow.js vendored Normal file
View File

@ -0,0 +1,13 @@
function fib(n){
if (n == 1) return 1;
if (n == 0) return 0;
if (n > 1) return fib(n - 2) + fib(n - 1)
}
function fi(){
console.log('fibonacci...');
var f = fib((parseInt(Math.random() * 10000) + 30) % 42);
console.log('is:', f);
setTimeout(fi, 1000);
}
fi();

5
test/fixtures/invalid.conf vendored Normal file
View File

@ -0,0 +1,5 @@
{
"pm2": "/tmp/.pm2",
"refresh": 3000,
"manipulation":
}

6
test/fixtures/pm2-gui-cp.conf vendored Normal file
View File

@ -0,0 +1,6 @@
{
"pm2": "/tmp/.pm2",
"refresh": 3000,
"manipulation": false,
"port": 27130
}

6
test/fixtures/pm2-gui.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"pm2": "~/.pm2",
"refresh": 5000,
"manipulation": true,
"port": 8088
}

3
test/fixtures/rand.js vendored Normal file
View File

@ -0,0 +1,3 @@
setInterval(function(){
console.log(Math.random());
}, 3000);

4
test/fixtures/throw.js vendored Normal file
View File

@ -0,0 +1,4 @@
console.log('App started.');
setTimeout(function(){
throw new Error('uncaughtException has been thrown.');
}, 15000);

4
test/fixtures/tick.js vendored Normal file
View File

@ -0,0 +1,4 @@
var chalk = require('chalk');
setInterval(function(){
console.log(chalk.bold.green('Tick'), Date.now());
}, 1000);

6
test/fixtures/tock.js vendored Normal file
View File

@ -0,0 +1,6 @@
var chalk = require('chalk');
console.log(chalk.magenta('Tock every 5 seconds.'));
setInterval(function(){
console.log(chalk.bold.red.underline('Tock'), Date.now());
}, 5000);

16
test/index.sh Normal file
View File

@ -0,0 +1,16 @@
#!/usr/bin/env bash
SRC=$(cd $(dirname "$0"); pwd)
source "${SRC}/bash/include.sh"
set -e
echo -e "\x1B[1m############ TEST SUITE ############\x1B[0m"
echo -e "\x1B[1mpm2-gui Command = $pg\x1B[0m"
echo -e "\x1B[1mNode version = $nodeVersion\x1B[0m"
$node -e "var os = require('os'); console.log('\x1B[1march : %s\nplatform : %s\nrelease : %s\ntype : %s\nmem : %d\x1B[0m', os.arch(), os.platform(), os.release(), os.type(), os.totalmem())"
echo -e "\x1B[1m############# FINISHED #############\x1B[0m"
echo -e ""
bash ./test/bash/config.sh
bash ./test/bash/interface.sh

View File

@ -8,7 +8,7 @@ var express = require('express'),
module.exports = runServer;
function runServer(port, debug){
function runServer(debug){
var app = express();
// all environments
@ -26,19 +26,16 @@ function runServer(port, debug){
// router
require('../lib/util/router')(app, log);
if (!port || isNaN(port)) {
port = 8088;
}
var server = require('http').Server(app);
var io = require('socket.io')(server);
server.listen(port);
log.i('http', 'Web server of', chalk.bold.underline('Unitech/PM2'), 'is listening on port', chalk.bold(port));
var mon = Monitor({
sockio: io,
debug : !!debug
});
var port = mon.config('port');
server.listen(port);
log.i('http', 'Web server of', chalk.bold.underline('Unitech/PM2'), 'is listening on port', chalk.bold(port));
mon.run();
}