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