Skip to content

Commit

Permalink
repl: add repl.setupHistory for programmatic repl
Browse files Browse the repository at this point in the history
Adds a `repl.setupHistory()` instance method so that
programmatic REPLs can also write history to a file.

This change also refactors all of the history file
management to `lib/internal/repl/history.js`, cleaning
up and simplifying `lib/internal/repl.js`.

PR-URL: #25895
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
  • Loading branch information
lance authored and addaleax committed Feb 13, 2019
1 parent 896962f commit 4c22d6e
Show file tree
Hide file tree
Showing 6 changed files with 422 additions and 158 deletions.
16 changes: 16 additions & 0 deletions doc/api/repl.md
Expand Up @@ -448,6 +448,22 @@ deprecated: v9.0.0
An internal method used to parse and execute `REPLServer` keywords.
Returns `true` if `keyword` is a valid keyword, otherwise `false`.

### replServer.setupHistory(historyPath, callback)
<!-- YAML
added: REPLACEME
-->

* `historyPath` {string} the path to the history file
* `callback` {Function} called when history writes are ready or upon error
* `err` {Error}
* `repl` {repl.REPLServer}

Initializes a history log file for the REPL instance. When executing the
Node.js binary and using the command line REPL, a history file is initialized
by default. However, this is not the case when creating a REPL
programmatically. Use this method to initialize a history log file when working
with REPL instances programmatically.

## repl.start([options])
<!-- YAML
added: v0.1.91
Expand Down
160 changes: 2 additions & 158 deletions lib/internal/repl.js
@@ -1,24 +1,10 @@
'use strict';

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

module.exports = Object.create(REPL);
module.exports.createInternalRepl = createRepl;

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

function _writeToOutput(repl, message) {
repl._writeToOutput(message);
repl._refreshLine();
}

function createRepl(env, opts, cb) {
if (typeof opts === 'function') {
cb = opts;
Expand Down Expand Up @@ -55,151 +41,9 @@ function createRepl(env, opts, cb) {
if (!Number.isNaN(historySize) && historySize > 0) {
opts.historySize = historySize;
} else {
// XXX(chrisdickinson): set here to avoid affecting existing applications
// using repl instances.
opts.historySize = 1000;
}

const repl = REPL.start(opts);
if (opts.terminal) {
return setupHistory(repl, env.NODE_REPL_HISTORY, cb);
}

repl._historyPrev = _replHistoryMessage;
cb(null, 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(), '.node_repl_history');
} catch (err) {
_writeToOutput(repl, '\nError: Could not get the home directory.\n' +
'REPL session history will not be persisted.\n');

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.
_writeToOutput(repl, '\nError: Could not open history file.\n' +
'REPL session history will not be persisted.\n');
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);
} else {
repl.history = [];
}

fs.open(historyPath, 'r+', onhandle);
}

function onhandle(err, hnd) {
if (err) {
return ready(err);
}
fs.ftruncate(hnd, 0, (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) {
_writeToOutput(
this,
'\nPersistent history support disabled. ' +
'Set the NODE_REPL_HISTORY environment\nvariable to ' +
'a valid, user-writable path to enable.\n'
);
}
this._historyPrev = Interface.prototype._historyPrev;
return this._historyPrev();
repl.setupHistory(opts.terminal ? env.NODE_REPL_HISTORY : '', cb);
}
153 changes: 153 additions & 0 deletions lib/internal/repl/history.js
@@ -0,0 +1,153 @@
'use strict';

const { Interface } = require('readline');
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;

module.exports = setupHistory;

function _writeToOutput(repl, message) {
repl._writeToOutput(message);
repl._refreshLine();
}

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(), '.node_repl_history');
} catch (err) {
_writeToOutput(repl, '\nError: Could not get the home directory.\n' +
'REPL session history will not be persisted.\n');

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.
_writeToOutput(repl, '\nError: Could not open history file.\n' +
'REPL session history will not be persisted.\n');
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);
} else {
repl.history = [];
}

fs.open(historyPath, 'r+', onhandle);
}

function onhandle(err, hnd) {
if (err) {
return ready(err);
}
fs.ftruncate(hnd, 0, (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(line) {
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) {
_writeToOutput(
this,
'\nPersistent history support disabled. ' +
'Set the NODE_REPL_HISTORY environment\nvariable to ' +
'a valid, user-writable path to enable.\n'
);
}
this._historyPrev = Interface.prototype._historyPrev;
return this._historyPrev();
}
5 changes: 5 additions & 0 deletions lib/repl.js
Expand Up @@ -83,6 +83,7 @@ const {
startSigintWatchdog,
stopSigintWatchdog
} = internalBinding('util');
const history = require('internal/repl/history');

// Lazy-loaded.
let processTopLevelAwait;
Expand Down Expand Up @@ -761,6 +762,10 @@ exports.start = function(prompt,
return repl;
};

REPLServer.prototype.setupHistory = function setupHistory(historyFile, cb) {
history(this, historyFile, cb);
};

REPLServer.prototype.clearBufferedCommand = function clearBufferedCommand() {
this[kBufferedCommandSymbol] = '';
};
Expand Down
1 change: 1 addition & 0 deletions node.gyp
Expand Up @@ -173,6 +173,7 @@
'lib/internal/readline.js',
'lib/internal/repl.js',
'lib/internal/repl/await.js',
'lib/internal/repl/history.js',
'lib/internal/repl/recoverable.js',
'lib/internal/socket_list.js',
'lib/internal/test/binding.js',
Expand Down

0 comments on commit 4c22d6e

Please sign in to comment.