diff --git a/src/cli_plugin/install/__tests__/rename.js b/src/cli_plugin/install/__tests__/rename.js new file mode 100644 index 0000000000000..8ff341bd222be --- /dev/null +++ b/src/cli_plugin/install/__tests__/rename.js @@ -0,0 +1,72 @@ +import expect from 'expect.js'; +import sinon from 'sinon'; +import fs from 'fs'; + +import { renamePlugin } from '../rename'; + +describe('plugin folder rename', function () { + let renameStub; + + beforeEach(function () { + renameStub = sinon.stub(); + }); + + afterEach(function () { + fs.rename.restore(); + }); + + it('should rethrow any exceptions', function () { + renameStub = sinon.stub(fs, 'rename', function (from, to, cb) { + cb({ + code: 'error' + }); + }); + + return renamePlugin('/foo/bar', '/bar/foo') + .catch(function (err) { + expect(err.code).to.be('error'); + expect(renameStub.callCount).to.be(1); + }); + }); + + it('should resolve if there are no errors', function () { + renameStub = sinon.stub(fs, 'rename', function (from, to, cb) { + cb(); + }); + + return renamePlugin('/foo/bar', '/bar/foo') + .then(function (err) { + expect(renameStub.callCount).to.be(1); + }) + .catch(function () { + throw new Error('We shouln\'t have any errors'); + }); + }); + + describe('Windows', function () { + let platform; + beforeEach(function () { + platform = Object.getOwnPropertyDescriptor(process, 'platform'); + Object.defineProperty(process, 'platform', { + value: 'win32' + }); + }); + afterEach(function () { + Object.defineProperty(process, 'platform', platform); + }); + + it('should retry on Windows EPERM errors for up to 3 seconds', function () { + this.timeout(5000); + renameStub = sinon.stub(fs, 'rename', function (from, to, cb) { + cb({ + code: 'EPERM' + }); + }); + return renamePlugin('/foo/bar', '/bar/foo') + .catch(function (err) { + expect(err.code).to.be('EPERM'); + expect(renameStub.callCount).to.be.greaterThan(1); + }); + }); + }); +}); diff --git a/src/cli_plugin/install/install.js b/src/cli_plugin/install/install.js index 1c72827cad71c..32bbbe79eb450 100644 --- a/src/cli_plugin/install/install.js +++ b/src/cli_plugin/install/install.js @@ -2,8 +2,8 @@ import { download } from './download'; import Promise from 'bluebird'; import { cleanPrevious, cleanArtifacts } from './cleanup'; import { extract, getPackData } from './pack'; +import { renamePlugin } from './rename'; import { sync as rimrafSync } from 'rimraf'; -import { renameSync } from 'fs'; import { existingInstall, rebuildCache, assertVersion } from './kibana'; import mkdirp from 'mkdirp'; @@ -27,7 +27,7 @@ export default async function install(settings, logger) { assertVersion(settings); - renameSync(settings.workingPath, settings.plugins[0].path); + await renamePlugin(settings.workingPath, settings.plugins[0].path); await rebuildCache(settings, logger); diff --git a/src/cli_plugin/install/rename.js b/src/cli_plugin/install/rename.js new file mode 100644 index 0000000000000..10f65c7657a16 --- /dev/null +++ b/src/cli_plugin/install/rename.js @@ -0,0 +1,21 @@ +import { rename } from 'fs'; +import { delay } from 'lodash'; + +export function renamePlugin(workingPath, finalPath) { + return new Promise(function (resolve, reject) { + const start = Date.now(); + const retryTime = 3000; + const retryDelay = 100; + rename(workingPath, finalPath, function retry(err) { + if (err) { + // In certain cases on Windows, such as running AV, plugin folders can be locked shortly after extracting + // Retry for up to retryTime seconds + const windowsEPERM = process.platform === 'win32' && err.code === 'EPERM'; + const retryAvailable = Date.now() - start < retryTime; + if (windowsEPERM && retryAvailable) return delay(rename, retryDelay, workingPath, finalPath, retry); + reject(err); + } + resolve(); + }); + }); +}