diff options
author | Lance Ball <lball@redhat.com> | 2019-02-01 20:49:16 +0300 |
---|---|---|
committer | Lance Ball <lball@redhat.com> | 2019-02-11 22:30:26 +0300 |
commit | 0aa74443d8bdea3c3840dbc3d4bd700b05ca7a4c (patch) | |
tree | 962e4c785ba03f01bf6ba5ad444f14e416c485ec /lib/internal/repl | |
parent | 902c71a9d09bce6b1da4258d1775375f6539bbec (diff) |
repl: add repl.setupHistory for programmatic repl
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: https://github.com/nodejs/node/pull/25895
Reviewed-By: Daniel Bevenius <daniel.bevenius@gmail.com>
Diffstat (limited to 'lib/internal/repl')
-rw-r--r-- | lib/internal/repl/history.js | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/lib/internal/repl/history.js b/lib/internal/repl/history.js new file mode 100644 index 00000000000..a0ae07441eb --- /dev/null +++ b/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(); +} |