Skip to content

Commit

Permalink
Command history support
Browse files Browse the repository at this point in the history
  • Loading branch information
Heath Morrison committed Jan 4, 2017
1 parent 352d834 commit b3259ad
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 7 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,12 @@ By integrating the loopback-console you also gain the ability to configure its f
The following configuration directives are supported,

- `quiet`: Suppresses the help text on startup and the automatic printing of `result`.
- `historyPath`: The path to a file to persist command history. Use an empty string (`''`) to disable history.
- All built-in configuration options for <a href="https://nodejs.org/api/repl.html" target="_blank">Node.js REPL</a>, such as `prompt`.
- `handles`: Disable any default handles, or pass additional handles that you would like available on the console.

Note, command history path can also be configured with the env-var `LOOPBACK_CONSOLE_HISTORY`.

## Contributors

- Heath Morrison (<a href="https://github.com/doublemarked" target="_blank">doublemarked</a>)
Expand Down
185 changes: 185 additions & 0 deletions history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* HM 2017-01-04:
* History processing was extracted from the internal Node REPL code with minimal changes,
* https://github.com/nodejs/node/blob/master/lib/internal/repl.js
*
* This internal history functionality is currently unpublished for various reasons
* discussed in the following PR:
* https://github.com/nodejs/node/pull/5789
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* All extracted code remains under the general nodejs license terms:
* https://github.com/nodejs/node/blob/master/LICENSE
*
* Copyright Node.js contributors. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to
* deal in the Software without restriction, including without limitation the
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/

'use strict';

const Interface = require('readline').Interface;
const path = require('path');
const fs = require('fs');
const os = require('os');
const util = require('util');
const debug = util.debuglog('repl');

// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary.
// The debounce is to guard against code pasted into the REPL.
const kDebounceHistoryMS = 15;

const DEFAULT_HISTORY_FILE = '.loopback_console_history';

module.exports = function initReplHistory(repl, historyPath) {
return new Promise((resolve, reject) => {
setupHistory(repl, historyPath, (err, repl) => {
if (err) {
reject(err);
} else {
resolve(repl);
}
});
});
};

function setupHistory(repl, historyPath, ready) {
// Empty string disables persistent history.
if (typeof historyPath === 'string')
historyPath = historyPath.trim();

if (historyPath === '') {
repl._historyPrev = _replHistoryMessage;
return ready(null, repl);
}

if (!historyPath) {
try {
historyPath = path.join(os.homedir(), DEFAULT_HISTORY_FILE);
} catch (err) {
repl._writeToOutput('\nError: Could not get the home directory.\n' +
'REPL session history will not be persisted.\n');
repl._refreshLine();

debug(err.stack);
repl._historyPrev = _replHistoryMessage;
return ready(null, repl);
}
}

var timer = null;
var writing = false;
var pending = false;
repl.pause();
// History files are conventionally not readable by others:
// https://github.com/nodejs/node/issues/3392
// https://github.com/nodejs/node/pull/3394
fs.open(historyPath, 'a+', 0o0600, oninit);

function oninit(err, hnd) {
if (err) {
// Cannot open history file.
// Don't crash, just don't persist history.
repl._writeToOutput('\nError: Could not open history file.\n' +
'REPL session history will not be persisted.\n');
repl._refreshLine();
debug(err.stack);

repl._historyPrev = _replHistoryMessage;
repl.resume();
return ready(null, repl);
}
fs.close(hnd, onclose);
}

function onclose(err) {
if (err) {
return ready(err);
}
fs.readFile(historyPath, 'utf8', onread);
}

function onread(err, data) {
if (err) {
return ready(err);
}

if (data) {
repl.history = data.split(/[\n\r]+/, repl.historySize);
}

fs.open(historyPath, 'w', onhandle);
}

function onhandle(err, hnd) {
if (err) {
return ready(err);
}
repl._historyHandle = hnd;
repl.on('line', online);

// reading the file data out erases it
repl.once('flushHistory', function() {
repl.resume();
ready(null, repl);
});
flushHistory();
}

// ------ history listeners ------
function online() {
repl._flushing = true;

if (timer) {
clearTimeout(timer);
}

timer = setTimeout(flushHistory, kDebounceHistoryMS);
}

function flushHistory() {
timer = null;
if (writing) {
pending = true;
return;
}
writing = true;
const historyData = repl.history.join(os.EOL);
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
}

function onwritten(err, data) {
writing = false;
if (pending) {
pending = false;
online();
} else {
repl._flushing = Boolean(timer);
if (!repl._flushing) {
repl.emit('flushHistory');
}
}
}
};

function _replHistoryMessage() {
if (this.history.length === 0) {
this._writeToOutput(
'\nPersistent history support disabled. ' +
'Set the history path to ' +
'a valid, user-writable path to enable.\n'
);
this._refreshLine();
}
this._historyPrev = Interface.prototype._historyPrev;
return this._historyPrev();
}
10 changes: 6 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DEFAULT_REPL_CONFIG = {
prompt: 'loopback > ',
useGlobal: true,
ignoreUndefined: true,
historyPath: process.env.LOOPBACK_CONSOLE_HISTORY,
};

const DEFAULT_HANDLE_INFO = {
Expand All @@ -24,7 +25,7 @@ const LoopbackConsole = module.exports = {

start(app, config) {
if (this._started) {
return this._ctx;
return Promise.resolve(this._ctx);
}
this._started = true;

Expand Down Expand Up @@ -58,9 +59,10 @@ const LoopbackConsole = module.exports = {
console.log(repl.usage(ctx));
}

ctx.repl = repl.start(ctx);

return ctx;
return repl.start(ctx).then(repl => {
ctx.repl = repl;
return ctx;
});
},

};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "loopback-console",
"version": "1.0.0",
"version": "1.1.0",
"description": "A command-line tool for Loopback app debugging and administration",
"main": "index.js",
"license": "MIT",
Expand Down
14 changes: 12 additions & 2 deletions repl.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const repl = require('repl');
const replHistory = require('./history');

const LoopbackRepl = module.exports = {
start(ctx) {
Expand All @@ -9,7 +10,6 @@ const LoopbackRepl = module.exports = {

Object.assign(replServer.context, ctx.handles);

replServer.on('exit', process.exit);
replServer.eval = wrapReplEval(replServer);

if (ctx.handles.cb === true) {
Expand Down Expand Up @@ -43,7 +43,17 @@ const LoopbackRepl = module.exports = {
},
});

return replServer;
replServer.on('exit', function() {
if (replServer._flushing) {
replServer.pause();
return replServer.once('flushHistory', function() {
process.exit();
});
}
process.exit();
});

return replHistory(replServer, config.historyPath).then(() => replServer);
},

usage(ctx, details) {
Expand Down

0 comments on commit b3259ad

Please sign in to comment.