'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; opts = null; } opts = { ignoreUndefined: false, terminal: process.stdout.isTTY, useGlobal: true, breakEvalOnSigint: true, ...opts }; if (parseInt(env.NODE_NO_READLINE)) { opts.terminal = false; } // The "dumb" special terminal, as defined by terminfo, doesn't support // ANSI color control codes. // see http://invisible-island.net/ncurses/terminfo.ti.html#toc-_Specials if (parseInt(env.NODE_DISABLE_COLORS) || env.TERM === 'dumb') { opts.useColors = false; } opts.replMode = { 'strict': REPL.REPL_MODE_STRICT, 'sloppy': REPL.REPL_MODE_SLOPPY }[String(env.NODE_REPL_MODE).toLowerCase().trim()]; if (opts.replMode === undefined) { opts.replMode = REPL.REPL_MODE_SLOPPY; } const historySize = Number(env.NODE_REPL_HISTORY_SIZE); 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(); }