Skip to content
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

Improved workarounds for win32 #119

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 44 additions & 11 deletions polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,58 @@ function patch (fs) {
fs.rename = (function (fs$rename) { return function (from, to, cb) {
var start = Date.now()
var backoff = 0;
var backoffUntil = start + 60000;
fs$rename(from, to, function CB (er) {
if (er
&& (er.code === "EACCES" || er.code === "EPERM")
&& Date.now() - start < 60000) {
if (er && (er.code === "EACCES" || er.code === "EPERM") && Date.now() < backoffUntil) {
setTimeout(function() {
fs.stat(to, function (stater, st) {
if (stater && stater.code === "ENOENT")
fs$rename(from, to, CB);
else
cb(er)
fs.stat(from, function (erFrom, statFrom) {
if (erFrom) return cb(erFrom)
fs.stat(to, function (erTo, statTo) {
if (statFrom.size === statTo.size &&
statFrom.ctime === statTo.ctime) {
cb(null)
} else
fs$rename(from, to, CB)
});
})
}, backoff)
if (backoff < 100)
if (backoff < 250)
backoff += 10;
return;
} else if (backoff && er && er.code === "ENOENT") {
// The source does no longer exist so we can assume it was moved during one of the tries
if (cb) cb(null)
} else {
if (cb) cb(er)
}
if (cb) cb(er)
})
}})(fs.rename)

fs.renameSync = (function (fs$renameSync) { return function (from, to) {
var start = Date.now()
var backoff = 0;
var backoffUntil = start + 60000;
function tryRename () {
try {
fs$renameSync(from, to)
} catch (e) {
if ((e.code === "EACCS" || e.code === "EPERM") && start < backoffUntil) {
if (backoff < 100)
backoff += 10
var waitUntil = Date.now() + backoff
while (waitUntil > Date.now()){}
tryRename()
} else if (backoff > 0 && e.code === "ENOENT") {
// The source does no longer exist because it was moved during one of the retries
// so we can safely ignore this exception
} else {
throw e
}
// Wait until destination exists and source no longer exists or that we've reached the backoff limit
while ((fs.existsSync(from) || !fs.existsSync(to)) && Date.now() < backoffUntil) {}
Copy link
Author

Choose a reason for hiding this comment

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

Possibly the worst hack ever, but it seems to be the only real way to solve inconsistent behavior on Windows where the destination and source can exist at the same time.

}
}
tryRename()
}})(fs.renameSync)
}

// if read() returns EAGAIN, then just try it again.
Expand Down
75 changes: 56 additions & 19 deletions test/windows-rename-polyfill.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,72 @@
process.env.GRACEFUL_FS_PLATFORM = 'win32'

var fs = require('fs')
fs.rename = function (a, b, cb) {
setTimeout(function () {
var er = new Error('EPERM blerg')
er.code = 'EPERM'
cb(er)
})
}

var gfs = require('../')
var t = require('tap')
var a = __dirname + '/a'
var b = __dirname + '/b'
var tmpDir = __dirname + '/tmp'
var testFiles = [];
var id = 0;

t.test('setup', function (t) {
try { fs.mkdirSync(a) } catch (e) {}
try { fs.mkdirSync(b) } catch (e) {}
function anyFileExists (files) {
var exists = false;
for (var i = 0, len = files.length; i < len; i++) {
if (fs.existsSync(files[i]))
return true
}
return false
}

t.test('setup async', function (t) {
try { fs.mkdirSync(tmpDir) } catch (e) {}
for (var i = 0; i < 500; i++) {
var testFile = tmpDir + '/test-' + id++
fs.writeFileSync(testFile, id)
testFiles.push(testFile)
}
t.end()
})

t.test('rename', { timeout: 100 }, function (t) {
t.plan(1)
t.test('rename async', { timeout: 60000 }, function (t) {
t.plan(testFiles.length * 2)
var dest = tmpDir + '/test'
testFiles.forEach((src) => {
gfs.rename(src, dest, function (er) {
t.error(er, 'Failed to rename file', er)
t.notOk(fs.existsSync(src), 'Source file still exists:' + src)
})
})
})

gfs.rename(a, b, function (er) {
t.ok(er)
t.test('setup sync', function (t) {
try { fs.mkdirSync(tmpDir) } catch (e) {}
testFiles = []
for (var i = 0; i < 100; i++) {
var testFile = tmpDir + '/test-' + id++
fs.writeFileSync(testFile, id)
testFiles.push(testFile)
}
t.end()
})

t.test('rename sync', { timeout: 60000 }, function (t) {
t.plan((testFiles.length * 2) + 1)
var done = 0;
var errors = 0;
var dest = tmpDir + '/test'
testFiles.forEach((src) => {
var srcData = fs.readFileSync(src).toString()
t.doesNotThrow(function () { gfs.renameSync(src, dest) }, 'Exception thrown when renaming')
var destData = fs.readFileSync(dest).toString()
t.equal(srcData, destData, 'Data between source and destination differs: ' + srcData + ' !== ' + destData)
})
t.notOk(anyFileExists(testFiles), 'Some source files still exist')
})

t.test('cleanup', function (t) {
try { fs.rmdirSync(a) } catch (e) {}
try { fs.rmdirSync(b) } catch (e) {}
testFiles.forEach(function (file) {
try { fs.removeSync(file) } catch (e) {}
})
try { fs.removeSync(tmpDir + '/test') } catch (e) {}
try { fs.rmdirSync(tmpDir) } catch (e) {}
t.end()
})