• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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