1'use strict'; 2 3const { 4 ArrayPrototypeJoin, 5 Boolean, 6 FunctionPrototype, 7 RegExpPrototypeSymbolSplit, 8 StringPrototypeTrim, 9} = primordials; 10 11const { Interface } = require('readline'); 12const path = require('path'); 13const fs = require('fs'); 14const os = require('os'); 15let debug = require('internal/util/debuglog').debuglog('repl', (fn) => { 16 debug = fn; 17}); 18const { clearTimeout, setTimeout } = require('timers'); 19 20const noop = FunctionPrototype; 21 22// XXX(chrisdickinson): The 15ms debounce value is somewhat arbitrary. 23// The debounce is to guard against code pasted into the REPL. 24const kDebounceHistoryMS = 15; 25 26module.exports = setupHistory; 27 28function _writeToOutput(repl, message) { 29 repl._writeToOutput(message); 30 repl._refreshLine(); 31} 32 33function setupHistory(repl, historyPath, ready) { 34 // Empty string disables persistent history 35 if (typeof historyPath === 'string') 36 historyPath = StringPrototypeTrim(historyPath); 37 38 if (historyPath === '') { 39 repl._historyPrev = _replHistoryMessage; 40 return ready(null, repl); 41 } 42 43 if (!historyPath) { 44 try { 45 historyPath = path.join(os.homedir(), '.node_repl_history'); 46 } catch (err) { 47 _writeToOutput(repl, '\nError: Could not get the home directory.\n' + 48 'REPL session history will not be persisted.\n'); 49 50 debug(err.stack); 51 repl._historyPrev = _replHistoryMessage; 52 return ready(null, repl); 53 } 54 } 55 56 let timer = null; 57 let writing = false; 58 let pending = false; 59 repl.pause(); 60 // History files are conventionally not readable by others: 61 // https://github.com/nodejs/node/issues/3392 62 // https://github.com/nodejs/node/pull/3394 63 fs.open(historyPath, 'a+', 0o0600, oninit); 64 65 function oninit(err, hnd) { 66 if (err) { 67 // Cannot open history file. 68 // Don't crash, just don't persist history. 69 _writeToOutput(repl, '\nError: Could not open history file.\n' + 70 'REPL session history will not be persisted.\n'); 71 debug(err.stack); 72 73 repl._historyPrev = _replHistoryMessage; 74 repl.resume(); 75 return ready(null, repl); 76 } 77 fs.close(hnd, onclose); 78 } 79 80 function onclose(err) { 81 if (err) { 82 return ready(err); 83 } 84 fs.readFile(historyPath, 'utf8', onread); 85 } 86 87 function onread(err, data) { 88 if (err) { 89 return ready(err); 90 } 91 92 if (data) { 93 repl.history = RegExpPrototypeSymbolSplit(/[\n\r]+/, data, repl.historySize); 94 } else { 95 repl.history = []; 96 } 97 98 fs.open(historyPath, 'r+', onhandle); 99 } 100 101 function onhandle(err, hnd) { 102 if (err) { 103 return ready(err); 104 } 105 fs.ftruncate(hnd, 0, (err) => { 106 repl._historyHandle = hnd; 107 repl.on('line', online); 108 repl.once('exit', onexit); 109 110 // Reading the file data out erases it 111 repl.once('flushHistory', function() { 112 repl.resume(); 113 ready(null, repl); 114 }); 115 flushHistory(); 116 }); 117 } 118 119 // ------ history listeners ------ 120 function online(line) { 121 repl._flushing = true; 122 123 if (timer) { 124 clearTimeout(timer); 125 } 126 127 timer = setTimeout(flushHistory, kDebounceHistoryMS); 128 } 129 130 function flushHistory() { 131 timer = null; 132 if (writing) { 133 pending = true; 134 return; 135 } 136 writing = true; 137 const historyData = ArrayPrototypeJoin(repl.history, os.EOL); 138 fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten); 139 } 140 141 function onwritten(err, data) { 142 writing = false; 143 if (pending) { 144 pending = false; 145 online(); 146 } else { 147 repl._flushing = Boolean(timer); 148 if (!repl._flushing) { 149 repl.emit('flushHistory'); 150 } 151 } 152 } 153 154 function onexit() { 155 if (repl._flushing) { 156 repl.once('flushHistory', onexit); 157 return; 158 } 159 repl.off('line', online); 160 fs.close(repl._historyHandle, noop); 161 } 162} 163 164function _replHistoryMessage() { 165 if (this.history.length === 0) { 166 _writeToOutput( 167 this, 168 '\nPersistent history support disabled. ' + 169 'Set the NODE_REPL_HISTORY environment\nvariable to ' + 170 'a valid, user-writable path to enable.\n', 171 ); 172 } 173 this._historyPrev = Interface.prototype._historyPrev; 174 return this._historyPrev(); 175} 176