Skip to content

Commit c307d1b

Browse files
authored
Retry after rename EPERM error (#119)
1 parent 0c4a992 commit c307d1b

File tree

3 files changed

+55
-3
lines changed

3 files changed

+55
-3
lines changed

lib/addRemove.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ function downloadAndExtractAsync(version) {
142142
childNames.forEach(childName => {
143143
let oldPath = path.join(extractedDirPath, childName);
144144
let newPath = path.join(targetDir, childName);
145-
fs.renameSync(oldPath, newPath);
145+
renameWithRetry(oldPath, newPath);
146146
});
147147

148148
// Remove the now-empty directory.
@@ -215,6 +215,38 @@ function remove(version) {
215215
return result;
216216
}
217217

218+
/**
219+
* Synchronously renames a file or directory, retrying to handle
220+
* occasional 'EPERM' or 'EACCS' errors.
221+
*/
222+
function renameWithRetry(from, to) {
223+
// Drived/simplified from https://github.com/isaacs/node-graceful-fs/pull/119
224+
let backoff = 0;
225+
const backoffUntil = Date.now() + 5000;
226+
function tryRename() {
227+
try {
228+
fs.renameSync(from, to);
229+
} catch (e) {
230+
if (!isWindows) {
231+
// The retry with backoff is only applicable to Windows.
232+
throw e;
233+
} else if ((e.code === 'EACCS' || e.code === 'EPERM') && Date.now() < backoffUntil) {
234+
if (backoff < 100) {
235+
backoff += 10;
236+
}
237+
const waitUntil = Date.now() + backoff;
238+
while (Date.now() < waitUntil) {}
239+
tryRename();
240+
} else if (backoff > 0 && e.code === 'ENOENT') {
241+
// The source no longer exists; assume it was renamed.
242+
} else {
243+
throw e;
244+
}
245+
}
246+
}
247+
tryRename();
248+
}
249+
218250
/**
219251
* Creates a hierarchy of directories as necessary.
220252
*/
@@ -286,10 +318,9 @@ async function fixNpmCmdShimsAsync(targetDir) {
286318
if (!isWindows) return;
287319

288320
try {
289-
// This assumes the npm version carried with the node installation
290-
// includes a `cmd-shim` module. Currently true for at least npm >= 3.
291321
const cmdShimPath = path.join(
292322
targetDir, 'node_modules', 'npm', 'node_modules', 'cmd-shim');
323+
fs.statSync(cmdShimPath);
293324
const cmdShim = require(cmdShimPath);
294325

295326
// Enumerate .cmd files in the target directory and fix if they are shims.
@@ -317,6 +348,12 @@ async function fixNpmCmdShimsAsync(targetDir) {
317348
});
318349
}
319350
} catch (e) {
351+
if (e.code === 'ENOENT') {
352+
// Currently all npm >= 3 include the cmd-shim module, but maybe
353+
// someday it won't? Also it does not exist with test mocking.
354+
return;
355+
}
356+
320357
// Not a fatal error. Most things still work if the shims are not fixed.
321358
// The only problem may be that the global npm package cannot be upgraded.
322359
console.warn('Warning: Failed to fix npm cmd shims: ' + e.message);
@@ -326,4 +363,5 @@ async function fixNpmCmdShimsAsync(targetDir) {
326363
module.exports = {
327364
addAsync,
328365
remove,
366+
renameWithRetry,
329367
};

test/mocks/fs.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const mockFs = {
1212
statMap: {},
1313
dataMap: {},
1414
unlinkPaths: [],
15+
nextRenameError: null,
1516

1617
reset() {
1718
this.trace = false;
@@ -183,6 +184,12 @@ const mockFs = {
183184
newPath = this.fixSep(newPath);
184185
if (this.trace) console.log('renameSync(' + oldPath, newPath + ')');
185186

187+
if (this.nextRenameError) {
188+
const e = this.nextRenameError;
189+
this.nextRenameError = null;
190+
throw e;
191+
}
192+
186193
if (this.dirMap[oldPath]) {
187194
// Support for renaming directories is limited to a single level.
188195
// Subdirectory paths currently do not get updated.

test/modules/addRemoveTests.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const test = require('ava').test;
55
const rewire = require('rewire');
66
const Error = require('../../lib/error');
77

8+
const isWindows = process.platform === 'win32';
9+
810
test.before(require('../checkNodeVersion'));
911

1012
const testHome = '/home/test/nvs/'.replace(/\//g, path.sep);
@@ -157,6 +159,11 @@ test('Add - download', t => {
157159
},
158160
});
159161

162+
if (isWindows) {
163+
// Simulate occasional EPERM during rename, which should be handled by a retry.
164+
mockFs.nextRenameError = new Error('Test rename error', 'EPERM');
165+
}
166+
160167
return nvsAddRemove.addAsync(version).then(message => {
161168
t.regex(message[0], /^Added at/);
162169
t.truthy(nvsUse.getVersionBinary(version));

0 commit comments

Comments
 (0)