Skip to content

Commit

Permalink
Multiple fixes and prettification of errors
Browse files Browse the repository at this point in the history
Fixes multiple outstanding issues, and pretifies error output by:

  1. using pretty-error to render the error
  2. stripping part of the stack that really should have no relevance

Fixes mage#4 mage#5 mage#8 mage#9 mage#11
  • Loading branch information
stelcheck committed Nov 26, 2017
1 parent 7a65cd7 commit 99dd1ad
Show file tree
Hide file tree
Showing 4 changed files with 1,620 additions and 67 deletions.
6 changes: 6 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ external:
# Where to put the socket file for the console.
# Default: [project-dir]/node_modules/mage-console/mage-console.sock
sockfile: /tmp/mage-console.sock

# Watch additional files; we will always watch `./config' and './lib',
# but you may want to watch additional folders as well.
watch:
- /tmp/file
- ./www
```
Debugging
Expand Down
243 changes: 176 additions & 67 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,16 @@ var chalk = require('chalk');
var path = require('path');
var cluster = require('cluster');
var fs = require('fs');
var PrettyError = require('pretty-error');
var watch = require('node-watch');
var MemoryStream = require('memorystream');

var HISTORY_FILE = path.join(process.env.HOME, '.mage-history.json');
var APP_LIB_PATH = path.join(process.cwd(), 'lib');
var APP_CONFIG_PATH = path.join(process.cwd(), 'config');

var debug = require('./debug');

function onceSomeFilesChanged(onChange) {
var called = false;
var watcher = watch(APP_LIB_PATH, {
recursive: true
}, function (event, name) {
if (name.split(path.sep).pop()[0] === '.') {
return;
}

if (called) {
return;
}

called = true;
watcher.close();
onChange(event, name);
});
}

/**
* exports.eval can be used to customise how the code and commands
* will be interpreted in the REPL interface - null means default node
* behaviour
*/
exports.eval = null;
var prettyError = new PrettyError();

/**
* Additional prefix to set on the REPL's prompt
Expand All @@ -51,25 +28,62 @@ exports.promptPrefix = '';
exports.debugFlag = '--debug';

/**
* Boot mage
* Force-configure mage to use cluster: 1 (one master, one process),
* and set the list of files and folders to watch for.
*/
var config = require('../mage/lib/config');
config.set('server.cluster', 1);

var WATCH_FILES = config.get('external.mage-console.watch', []);

WATCH_FILES.push(APP_CONFIG_PATH);
WATCH_FILES.push(APP_LIB_PATH);

/**
* Load the application and boot
*/
var mage = require(APP_LIB_PATH);
var logger = mage.core.logger.context('REPL');
var processManager = mage.core.processManager;

mage.boot();

var clusterConfiguration = mage.core.config.get('server.cluster');
function setRawMode(val) {
var stdin = process.stdin;

if (!stdin.setRawMode) {
logger
.emergency
.details('This may happen when using terminals such as MINGW64 on Windows')
.details('Please try to use PowerShell or cmd.exe instead')
.log('Cannot run mage-console; cannot switch to raw mode');

if (clusterConfiguration !== 1) {
console.error('');
console.error('mage-console requires your application to be configured with');
console.error('"server.cluster" to 1. Please change your configuration and try again');
console.error('');
mage.quit(1, true);
}

process.exit(-1);
stdin.setRawMode(val);
}

var logger = mage.core.logger.context('REPL');
function onceSomeFilesChanged(onChange) {
var called = false;
var watcher = watch(WATCH_FILES, {
recursive: true
}, function (event, name) {
if (name.split(path.sep).pop()[0] === '.') {
return;
}

if (called) {
return;
}

called = true;
watcher.close();
onChange(event, name);
});

return watcher;
}

function crash(error) {
mage.logger.error(error);
Expand All @@ -83,7 +97,7 @@ function crash(error) {
function getIPCPath() {
// See https://github.com/nodejs/node/issues/13670 for more details
const defaultFilepath = path.relative(process.cwd(), path.join(__dirname, 'mage-console.sock'));
const filepath = mage.core.config.get('external.mage-console.sockfile') || defaultFilepath;
const filepath = config.get('external.mage-console.sockfile') || defaultFilepath;

if (process.platform === 'win32') {
return path.join('\\\\.\\pipe', filepath);
Expand All @@ -110,10 +124,57 @@ function createRepl(client, prompt) {
output: client,
useColors: true,
terminal: true,
prompt: prompt,
eval: exports.eval
prompt: prompt
});

// We wish to log REPL errors differently than how
// we log errors coming from MAGE logger, but still
// in a way that integrates with the logger (so that logs
// may still be written to file, and so on); we want the logger
// context, and we want to prettify the error output.
function logError(error) {
var newStack = error.stack.split('\n');
var done = false;
error.stack = '';

while (!done && newStack.length > 0) {
var line = newStack.shift();
error.stack += line + '\n';
if (line.indexOf(' at repl:1') !== -1) {
done = true;
}
}

var rendered = prettyError.render(error);
rendered = rendered.substring(0, rendered.length - 5);
rendered = rendered.replace(/\n/g, chalk.styles.red.open + '\n');
logger.error(rendered);
}

// We remove the default domain error handler on the REPL
// and replace it with our logging factory
instance._domain.removeAllListeners('error');
instance._domain.on('error', logError);

// Finally, we override the eval function.
// since we want to keep the same behavior as the normal eval
// but handle errors a bit differently, we create a normal REPL,
// store it's eval method somewhere, then override it.
var realEval = instance.eval;
instance.eval = function (cmd, context, filename, callback) {
realEval(cmd, context, filename, function (error, res) {
if (error) {
if (!error.stack) {
return callback(error);
}

logError(error);
}

callback(null, res);
});
};

// Context setup
instance.context.mage = mage;

Expand Down Expand Up @@ -153,7 +214,7 @@ function connect() {
chalk.cyan('mage/' + mage.rootPackage.name) +
chalk.magenta(' >> ');

var promptLength = chalk.stripColor(prompt).length;
var promptLength = prompt.length;
var repl = createRepl(client, prompt);

repl.on('exit', function () {
Expand All @@ -171,7 +232,7 @@ function connect() {
input: stream
});

function schedulePrompt(lineContent) {
function schedulePrompt() {
if (closing) {
return;
}
Expand All @@ -181,8 +242,7 @@ function connect() {
}

scheduled = setTimeout(function () {
realStderrWrite(prompt);
realStderrWrite(lineContent);
repl.displayPrompt(true);
}, 100);
}

Expand All @@ -201,7 +261,7 @@ function connect() {
realStderrWrite(`\r${wipeLine}\r`);
realStderrWrite(data + '\n');

schedulePrompt(lineContent);
schedulePrompt();
});

// Watch the lib folder for changes
Expand All @@ -212,6 +272,18 @@ function connect() {
});
});

// Propagate the stdout resize events to the client, so that the readline
// interface behind our REPL may process properly commands that fit on
// multiple lines
client.columns = process.stdout.columns;
client.rows = process.stdout.rows;

process.stdout.on('resize', function () {
client.columns = process.stdout.columns;
client.rows = process.stdout.rows;
client.emit('resize');
});

client.once('end', function () {
closing = true;
connect();
Expand All @@ -227,27 +299,58 @@ if (cluster.isWorker) {

// Master process provides a network server for process
// to connect to, and patches stdin/stdout/stderr into
// the connection
// the connection. It also hijacks MAGE's reload logic, so
// to allow for pauses while waiting for a restart (example:
// if the server reloads and crashes, wait for a file change
// before restarting)

processManager.on('started', function () {
cluster.removeAllListeners('exit');
cluster.on('exit', function (worker) {
worker.dropStartupTimeout();

var id = worker.mageWorkerId;
processManager.emit('workerOffline', id, worker._mageManagedExit);

// If a worker was running and it suddenly dies, we automatically
// restart it. If not, we consider something must be wrong with the
// application's code, and stall until either a file is updated or
// a key is pressed in the terminal
if (!worker._mageManagedExit) {
logger.debug('Reloading');
processManager.getWorkerManager().createWorker(id);
} else {
console.log('');
logger.warning('------------------------------------------------------');
logger.warning('Worker down, save a file of press any key to reload...');
logger.warning('------------------------------------------------------');

var stdin = process.stdin;
var waiter;

function onKeyPress() {
if (waiter) {
waiter.close();
}

stdin.pause();

processManager.getWorkerManager().createWorker(id);
}

cluster.removeAllListeners('exit');
cluster.on('exit', function (worker) {
// check if the worker was supposed to die
waiter = onceSomeFilesChanged(function (event, name) {
stdin.removeListener('data', onKeyPress);
stdin.pause();

if (!worker._mageManagedExit) {
// this exit was not supposed to happen!
// spawn a new worker to replace the dead one
logger.debug('File ' + name + ' was ' + event + 'd, reloading');

processManager.emit('workerOffline', worker.id);
onceSomeFilesChanged(function (event, name) {
logger.debug('File ' + name + ' was ' + event + 'd, reloading');
processManager.createWorker();
});
} else {
if (!processManager.getNumWorkers()) {
logger.emergency('All workers have shut down, shutting down master now.');
process.exit(0);
processManager.getWorkerManager().createWorker(id);
});

stdin.resume();
stdin.once('data', onKeyPress);
}
}
});
});

// Clean up old lingering sockets
Expand Down Expand Up @@ -310,14 +413,19 @@ cluster.on('message', function (worker, message) {
case 'reload':
closeDebuggerConnection();
logger.notice('reloading worker');
mage.core.processManager.reload(function () {
logger.notice('worker reloaded');
});
worker.kill();
break;
case 'shutdown':
closeDebuggerConnection();
logger.notice('shutting down');
mage.quit();
cluster.removeAllListeners('exit');
worker.once('exit', function () {
closeDebuggerConnection();
mage.quit();
process.exit();
});

worker.kill();

break;
}
});
Expand All @@ -327,15 +435,16 @@ var server = net.createServer(function (client) {
logger.notice('connected');

var stdin = process.stdin;
stdin.setRawMode(true);
setRawMode(true);

stdin.setEncoding('utf8');
stdin.resume();

client.pipe(process.stdout);
stdin.pipe(client);

client.on('end', function () {
stdin.setRawMode(false);
setRawMode(false);
stdin.pause();
logger.notice('disconnected');
});
Expand Down
Loading

0 comments on commit 99dd1ad

Please sign in to comment.