Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple fixes and prettification of errors #12

Merged
merged 1 commit into from
Dec 6, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the ../ before mage? Module resolution should find mage/lib/config just fine, right?

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] === '.') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this code existed before, but what is this about? Hidden files? Why ignore them exactly? (.DS_Store and stuff?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. IIRC WebStorm and some other IDEs create and update dot files on file save; this was a quick fix to ensure file watch woild not trigger more than once (arguably not a real fix, but in this case it got the job done).

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.slice(0, -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 or 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 @@ -323,14 +426,19 @@ cluster.on('message', function (worker, message) {
case 'reload':
closeDebuggerConnections();
logger.notice('reloading worker');
mage.core.processManager.reload(function () {
logger.notice('worker reloaded');
});
worker.kill();
break;
case 'shutdown':
closeDebuggerConnections();
logger.notice('shutting down');
mage.quit();
cluster.removeAllListeners('exit');
worker.once('exit', function () {
closeDebuggerConnection();
mage.quit();
process.exit();
});

worker.kill();

break;
}
});
Expand All @@ -340,15 +448,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