'use strict'; const { ArrayPrototypeJoin, Boolean, FunctionPrototype, RegExpPrototypeSymbolSplit, StringPrototypeTrim, } = primordials; const { Interface } = require('readline'); const path = require('path'); const fs = require('fs'); const os = require('os'); let debug = require('internal/util/debuglog').debuglog('repl', (fn) => { debug = fn; }); const { clearTimeout, setTimeout } = require('timers'); const noop = FunctionPrototype; // 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 = StringPrototypeTrim(historyPath); 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); } } let timer = null; let writing = false; let 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 = RegExpPrototypeSymbolSplit(/[\n\r]+/, data, 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); repl.once('exit', onexit); // 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 = ArrayPrototypeJoin(repl.history, 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 onexit() { if (repl._flushing) { repl.once('flushHistory', onexit); return; } repl.off('line', online); fs.close(repl._historyHandle, noop); } } 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(); }