Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 36.4k
"Fix" repl race condition#1677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
de5a5f5ba80aa146e2d68e4f0cfc9b9dd708752fe1bdf2b9f821332b6158e2dFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| @@ -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)) { | ||
| opts.terminal = true; | ||
| } | ||
| if (parseInt(env.NODE_DISABLE_COLORS)) { | ||
| opts.useColors = false; | ||
| } | ||
| @@ -51,8 +56,13 @@ function createRepl(env, cb) { | ||
| } | ||
| const repl = REPL.start(opts); | ||
| if (opts.terminal && env.NODE_REPL_HISTORY_FILE) { | ||
| return setupHistory(repl, env.NODE_REPL_HISTORY_FILE, cb); | ||
| if (env.NODE_REPL_HISTORY_FILE && attemptPersistentHistory) { | ||
| return setupHistory(repl, env.NODE_REPL_HISTORY_FILE, function(err) { | ||
| if (err) { | ||
| repl.close(); | ||
| } | ||
| cb(err, repl); | ||
| }); | ||
| } | ||
| repl._historyPrev = _replHistoryMessage; | ||
| cb(null, repl); | ||
| @@ -64,27 +74,14 @@ function setupHistory(repl, historyPath, ready) { | ||
| var writing = false; | ||
| var pending = false; | ||
| repl.pause(); | ||
| fs.open(historyPath, 'a+', oninit); | ||
| 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); | ||
| } | ||
| fs.readFile(historyPath, {encoding: 'utf8', flag: 'a+'}, ondata); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 onread(err, data) { | ||
| function ondata(err, data) { | ||
| if (err) { | ||
| return ready(err); | ||
| } | ||
| debug('loaded %s', historyPath); | ||
| if (data) { | ||
| try { | ||
| repl.history = JSON.parse(data); | ||
| @@ -98,17 +95,16 @@ function setupHistory(repl, historyPath, ready) { | ||
| } | ||
| } | ||
| fs.open(historyPath, 'w', onhandle); | ||
| getTempFile(ontmpfile); | ||
| } | ||
| function onhandle(err, hnd) { | ||
| function ontmpfile(err, tmpinfo) { | ||
| if (err) { | ||
| return ready(err); | ||
| } | ||
| repl._historyHandle = hnd; | ||
| repl._historyTempInfo = tmpinfo; | ||
| repl.on('line', online); | ||
| // reading the file data out erases it | ||
| repl.on('exit', removeTempFile); | ||
| repl.once('flushHistory', function() { | ||
| repl.resume(); | ||
| ready(null, repl); | ||
| @@ -134,11 +130,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); | ||
| writeAndSwapTemp(historyData, repl._historyTempInfo, historyPath, onflushed); | ||
| } | ||
| function onwritten(err, data) { | ||
| function onflushed(err, data) { | ||
| writing = false; | ||
| if (pending) { | ||
| pending = false; | ||
| @@ -150,6 +146,17 @@ function setupHistory(repl, historyPath, ready) { | ||
| } | ||
| } | ||
| } | ||
| function removeTempFile() { | ||
| if (repl._flushing) | ||
| return repl.once('flushHistory', function() { | ||
| removeTempFile(); | ||
| }); | ||
| repl._flushing = true; | ||
| fs.unlink(repl._historyTempInfo.path, function() { | ||
| onflushed(); | ||
| }); | ||
| } | ||
| } | ||
| @@ -165,3 +172,63 @@ function _replHistoryMessage() { | ||
| this._historyPrev = Interface.prototype._historyPrev; | ||
| return this._historyPrev(); | ||
| } | ||
| function getTempFile(ready) { | ||
| var tmpPath = os.tmpdir(); | ||
| if (!tmpPath) { | ||
| return ready(new Error('no tmpdir available')); | ||
| } | ||
| tmpPath = path.join(tmpPath, `${process.pid}-node-repl.tmp`); | ||
| fs.open(tmpPath, 'wx', 0o600, function(err, fd) { | ||
| 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, and open a new tmpfile. | ||
| // | ||
| // We use fs.write instead of writeFile because the latter | ||
| // does not accept an fd at the time of writing. | ||
| function writeAndSwapTemp(data, tmpInfo, target, ready) { | ||
| debug('writing tmp file... %s', tmpInfo.path, data); | ||
| return fs.write(tmpInfo.fd, data, 0, 'utf8', onwritten); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seeing that you're using EDIT: Never mind, I thought that ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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); | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)? Member There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...'); | ||
| getTempFile(ontmpfile); | ||
| } | ||
| function ontmpfile(err, info) { | ||
| if (err) return ready(err); | ||
| tmpInfo.fd = info.fd; | ||
| tmpInfo.path = info.path; | ||
| debug('done!'); | ||
| return ready(null); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -131,9 +131,20 @@ | ||
| 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.error('Encountered error with persistent history support.') | ||
| console.error('Run with NODE_DEBUG=repl for more information.'); | ||
| if (/repl/.test(process.env.NODE_DEBUG || '')) { | ||
| console.error(err.stack); | ||
| } | ||
| return cliRepl.createInternalRepl( | ||
| process.env, false, function(err, repl) { | ||
ContributorAuthor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like an odd style choice, but Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
| 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); | ||
| }) | ||
| }) | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.