Skip to content
Closed
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ bench-idle:
./$(NODE_EXE) benchmark/idle_clients.js &

jslint:
./$(NODE_EXE) tools/eslint/bin/eslint.js src/*.js lib/*.js --reset --quiet
./$(NODE_EXE) tools/eslint/bin/eslint.js src/*.js lib/*.js lib/internal/*.js --reset --quiet

CPPLINT_EXCLUDE ?=
CPPLINT_EXCLUDE += src/node_lttng.cc
Expand Down
100 changes: 74 additions & 26 deletions lib/internal/repl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
const Interface = require('readline').Interface;
const REPL = require('repl');
const path = require('path');
const util = require('util');

const debug = util.debuglog('repl');

module.exports = Object.create(REPL);
module.exports.createInternalRepl = createRepl;
Expand All @@ -17,16 +20,18 @@ function replStart() {
return REPL.start.apply(REPL, arguments);
}

function createRepl(env, cb) {
function createRepl(env, attemptPersistentHistory, cb) {
const opts = {
ignoreUndefined: false,
terminal: process.stdout.isTTY,
useGlobal: true
};

if (parseInt(env.NODE_NO_READLINE)) {
opts.terminal = false;
}
if (parseInt(env.NODE_FORCE_READLINE)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the radix here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't be a problem in this case – we're just detecting whether the number is "0" or not.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we can explicitly do that right? As it is, the intention of this code is not clear, IMO.

opts.terminal = true;
}
if (parseInt(env.NODE_DISABLE_COLORS)) {
opts.useColors = false;
}
Expand All @@ -51,7 +56,7 @@ function createRepl(env, cb) {
}

const repl = REPL.start(opts);
if (opts.terminal && env.NODE_REPL_HISTORY_FILE) {
if (env.NODE_REPL_HISTORY_FILE && attemptPersistentHistory) {
return setupHistory(repl, env.NODE_REPL_HISTORY_FILE, cb);
}
repl._historyPrev = _replHistoryMessage;
Expand All @@ -64,27 +69,14 @@ function setupHistory(repl, historyPath, ready) {
var writing = false;
var pending = false;
repl.pause();
fs.open(historyPath, 'a+', oninit);
fs.readFile(historyPath, {encoding: 'utf8', flag: 'a+'}, ondata);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does 'a+' make sense when reading from a file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the docs, I'm using "a+" for "create file if it doesn't exist, retain contents for reading if it does."


function oninit(err, hnd) {
if (err) {
return ready(err);
}
fs.close(hnd, onclose);
}

function onclose(err) {
if (err) {
return ready(err);
}
fs.readFile(historyPath, 'utf8', onread);
}

function onread(err, data) {
function ondata(err, data) {
if (err) {
return ready(err);
}

debug('loaded %s', historyPath);
if (data) {
try {
repl.history = JSON.parse(data);
Expand All @@ -98,17 +90,16 @@ function setupHistory(repl, historyPath, ready) {
}
}

fs.open(historyPath, 'w', onhandle);
getTMPFile(ontmpfile);
}

function onhandle(err, hnd) {
function ontmpfile(err, tmpinfo) {
if (err) {
return ready(err);
}
repl._historyHandle = hnd;
repl._historyTMPInfo = tmpinfo;
repl.on('line', online);

// reading the file data out erases it
repl.once('flushHistory', function() {
repl.resume();
ready(null, repl);
Expand All @@ -134,11 +125,11 @@ function setupHistory(repl, historyPath, ready) {
return;
}
writing = true;
const historyData = JSON.stringify(repl.history, null, 2);
fs.write(repl._historyHandle, historyData, 0, 'utf8', onwritten);
const historyData = JSON.stringify(repl.history || [], null, 2);
writeAndSwapTMP(historyData, repl._historyTMPInfo, historyPath, onflushed);
}

function onwritten(err, data) {
function onflushed(err, data) {
writing = false;
if (pending) {
pending = false;
Expand All @@ -165,3 +156,60 @@ function _replHistoryMessage() {
this._historyPrev = Interface.prototype._historyPrev;
return this._historyPrev();
}


function getTMPFile(ready) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe getTempFile? Its current name is a little heavy on capitals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed (replaced all "TMP" with "Temp")

var tmpPath = os.tmpdir();
if (!tmpPath) {
return ready(new Error('no tmpdir available'));
}
tmpPath = path.join(tmpPath, process.pid + '-' + 'repl.tmp');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe 'node-repl-' + process.pid to make it clear which process created that file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

fs.open(tmpPath, 'w', 0o600, function(err, fd) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mode should probably be 'wx' to prevent symlink attacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

if (err) {
return ready(err);
}
return ready(null, {
fd: fd,
path: tmpPath
});
});
}

// write data, sync the fd, close it, overwrite the target
// with a rename, open a new tmpfile
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you capitalize and punctuate comments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

function writeAndSwapTMP(data, tmpInfo, target, ready) {
debug('writing tmp file... %s', tmpInfo.path, data);
return fs.write(tmpInfo.fd, data, 0, 'utf8', onwritten);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing that you're using fs.readFile() above, maybe use fs.writeFile() here?

EDIT: Never mind, I thought that fs.writeFile() accepted a file descriptor these days but it doesn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note about this so future humans know to change this if writeFile gains fd support.


function onwritten(err) {
if (err) return ready(err);
debug('syncing... %s', tmpInfo.path);
fs.fsync(tmpInfo.fd, onsync);
}

function onsync(err) {
if (err) return ready(err);
debug('closing... %s', tmpInfo.path);
fs.close(tmpInfo.fd, onclose);
}

function onclose(err) {
if (err) return ready(err);
debug('rename... %s -> %s', tmpInfo.path, target);
fs.rename(tmpInfo.path, target, onrename);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a regrettable race condition here... maybe add a comment so people don't think it's an oversight.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A race condition between processes renaming their tmpfiles, or a race condition within the process itself (that I missed)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could swear I'd replied... a race window between creating and renaming the temporary file. In theory, something like a cron job can delete the file in that time window.

There is also possibly an issue with multiple iojs processes terminating at the same time, e.g. because the login session is terminated.

}

function onrename(err) {
if (err) return ready(err);
debug('re-loading...');
getTMPFile(ontmpfile);
}

function ontmpfile(err, info) {
if (err) return ready(err);
tmpInfo.fd = info.fd;
tmpInfo.path = info.path;
debug('done!');
return ready(null);
}
}
11 changes: 9 additions & 2 deletions src/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,16 @@
if (process._forceRepl || NativeModule.require('tty').isatty(0)) {
// REPL
var cliRepl = Module.requireRepl();
cliRepl.createInternalRepl(process.env, function(err, repl) {
cliRepl.createInternalRepl(process.env, true, function(err, repl) {
if (err) {
throw err;
console.log('Encountered error with persistent history support.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not console.error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed + made more explicit & gave remediation steps.

return cliRepl.createInternalRepl(
process.env, false, function(err, repl) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an odd style choice, but make jslint didn't complain!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter is set up pretty conservative right now. Not sure it has a rule for odd linebreaks, but it seems fine to me there.

if (err) {
throw err;
}
repl.on('exit', process.exit);
});
}
repl.on('exit', function() {
if (repl._flushing) {
Expand Down
60 changes: 60 additions & 0 deletions test/parallel/test-internal-repl-race.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
var spawn = require('child_process').spawn;
var common = require('../common');
var assert = require('assert');
var path = require('path');
var os = require('os');
var fs = require('fs');

var filename = path.join(common.tmpDir, 'node-history.json');

var config = {
env: {
NODE_REPL_HISTORY_FILE: filename,
NODE_FORCE_READLINE: 1
}
}

var lhs = spawn(process.execPath, ['-i'], config);
var rhs = spawn(process.execPath, ['-i'], config);

lhs.stdout.pipe(process.stdout);
rhs.stdout.pipe(process.stdout);
lhs.stderr.pipe(process.stderr);
rhs.stderr.pipe(process.stderr);


var i = 0;
var tried = 0;
var exitcount = 0;
while (i++ < 100) {
lhs.stdin.write('hello = 0' + os.EOL);
rhs.stdin.write('OK = 0' + os.EOL);
}
lhs.stdin.write('\u0004')
rhs.stdin.write('\u0004')

lhs.once('exit', onexit)
rhs.once('exit', onexit)

function onexit() {
if (++exitcount !== 2) {
return;
}
check();
}

function check() {
fs.readFile(filename, 'utf8', function(err, data) {
if (err) {
console.log(err)
if (++tried > 100) {
throw err;
}
return setTimeout(check, 15);
}
assert.doesNotThrow(function() {
console.log(data)
JSON.parse(data);
})
})
}