From a36812ab848b4d3c89bcd996833068d8057ffc49 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Thu, 8 Mar 2018 12:10:29 -0800 Subject: [PATCH] For #43813 - retry rename when it fails with EPERM on windows, for 5s --- .../node/extensionManagementService.ts | 205 ++++++++++-------- 1 file changed, 113 insertions(+), 92 deletions(-) diff --git a/src/vs/platform/extensionManagement/node/extensionManagementService.ts b/src/vs/platform/extensionManagement/node/extensionManagementService.ts index 9f3df878827..189ea355193 100644 --- a/src/vs/platform/extensionManagement/node/extensionManagementService.ts +++ b/src/vs/platform/extensionManagement/node/extensionManagementService.ts @@ -30,7 +30,7 @@ import Event, { Emitter } from 'vs/base/common/event'; import * as semver from 'semver'; import URI from 'vs/base/common/uri'; import pkg from 'vs/platform/node/package'; -import { isMacintosh } from 'vs/base/common/platform'; +import { isMacintosh, platform, Platform } from 'vs/base/common/platform'; import { ILogService } from 'vs/platform/log/common/log'; import { ExtensionsManifestCache } from 'vs/platform/extensionManagement/node/extensionsManifestCache'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; @@ -101,6 +101,8 @@ interface InstallableExtension { export class ExtensionManagementService extends Disposable implements IExtensionManagementService { + private static readonly RENAME_RETRY_TIME = 5 * 1000; + _serviceBrand: any; private extensionsPath: string; @@ -147,25 +149,25 @@ export class ExtensionManagementService extends Disposable implements IExtension const identifier = { id: getLocalExtensionIdFromManifest(manifest) }; return this.unsetUninstalledAndRemove(identifier.id) .then( - () => this.checkOutdated(manifest) - .then(validated => { - if (validated) { - this.logService.info('Installing the extension:', identifier.id); - this._onInstallExtension.fire({ identifier, zipPath }); - return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) - .then( - metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest), - error => this.installFromZipPath(identifier, zipPath, null, manifest)) - .then( - local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, - e => { - this.logService.error('Failed to install the extension:', identifier.id, e.message); - return TPromise.wrapError(e); - }); - } - return null; - }), - e => TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name)))); + () => this.checkOutdated(manifest) + .then(validated => { + if (validated) { + this.logService.info('Installing the extension:', identifier.id); + this._onInstallExtension.fire({ identifier, zipPath }); + return this.getMetadata(getGalleryExtensionId(manifest.publisher, manifest.name)) + .then( + metadata => this.installFromZipPath(identifier, zipPath, metadata, manifest), + error => this.installFromZipPath(identifier, zipPath, null, manifest)) + .then( + local => { this.logService.info('Successfully installed the extension:', identifier.id); return local; }, + e => { + this.logService.error('Failed to install the extension:', identifier.id, e.message); + return TPromise.wrapError(e); + }); + } + return null; + }), + e => TPromise.wrapError(new Error(nls.localize('restartCode', "Please restart Code before reinstalling {0}.", manifest.displayName || manifest.name)))); }); } @@ -220,8 +222,8 @@ export class ExtensionManagementService extends Disposable implements IExtension return local; }) .then( - local => { this._onDidInstallExtension.fire({ identifier, zipPath, local }); return local; }, - error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); } + local => { this._onDidInstallExtension.fire({ identifier, zipPath, local }); return local; }, + error => { this._onDidInstallExtension.fire({ identifier, zipPath, error }); return TPromise.wrapError(error); } ); } @@ -229,17 +231,17 @@ export class ExtensionManagementService extends Disposable implements IExtension this.onInstallExtensions([extension]); return this.collectExtensionsToInstall(extension) .then( - extensionsToInstall => { - if (extensionsToInstall.length > 1) { - this.onInstallExtensions(extensionsToInstall.slice(1)); - } - return this.downloadAndInstallExtensions(extensionsToInstall) - .then( - locals => this.onDidInstallExtensions(extensionsToInstall, locals, []) - .then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier)[0])), - errors => this.onDidInstallExtensions(extensionsToInstall, [], errors)); - }, - error => this.onDidInstallExtensions([extension], [], [error])); + extensionsToInstall => { + if (extensionsToInstall.length > 1) { + this.onInstallExtensions(extensionsToInstall.slice(1)); + } + return this.downloadAndInstallExtensions(extensionsToInstall) + .then( + locals => this.onDidInstallExtensions(extensionsToInstall, locals, []) + .then(() => locals.filter(l => areSameExtensions({ id: getGalleryExtensionIdFromLocal(l), uuid: l.identifier.uuid }, extension.identifier)[0])), + errors => this.onDidInstallExtensions(extensionsToInstall, [], errors)); + }, + error => this.onDidInstallExtensions([extension], [], [error])); } reinstall(extension: ILocalExtension): TPromise { @@ -252,8 +254,8 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.uninstallExtension(extension) .then(() => this.removeUninstalledExtension(extension) .then( - () => this.installFromGallery(galleryExtension), - e => TPromise.wrapError(new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e)))))); + () => this.installFromGallery(galleryExtension), + e => TPromise.wrapError(new Error(nls.localize('removeError', "Error while removing the extension: {0}. Please Quit and Start VS Code before trying again.", toErrorMessage(e)))))); } return TPromise.wrapError(new Error(nls.localize('Not a Marketplace extension', "Only Marketplace Extensions can be reinstalled"))); }); @@ -267,10 +269,10 @@ export class ExtensionManagementService extends Disposable implements IExtension } return this.getDependenciesToInstall(compatible.properties.dependencies) .then( - dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]), - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); + dependenciesToInstall => ([compatible, ...dependenciesToInstall.filter(d => d.identifier.uuid !== compatible.identifier.uuid)]), + error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); }, - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); + error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); } private downloadAndInstallExtensions(extensions: IGalleryExtension[]): TPromise { @@ -292,8 +294,8 @@ export class ExtensionManagementService extends Disposable implements IExtension .then(extension => this.downloadInstallableExtension(extension)) .then(installableExtension => this.installExtension(installableExtension)) .then( - local => { this.installingExtensions.delete(extension.identifier.id); return local; }, - e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); } + local => { this.installingExtensions.delete(extension.identifier.id); return local; }, + e => { this.installingExtensions.delete(extension.identifier.id); return TPromise.wrapError(e); } ); this.installingExtensions.set(extension.identifier.id, installingExtension); @@ -310,25 +312,25 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.galleryService.loadCompatibleVersion(extension) .then( - compatible => { - if (compatible) { - this.logService.trace('Started downloading extension:', extension.name); - return this.galleryService.download(extension) - .then( - zipPath => { - this.logService.info('Downloaded extension:', extension.name); - return validateLocalExtension(zipPath) - .then( - manifest => ({ zipPath, id: getLocalExtensionIdFromManifest(manifest), metadata }), - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING)) - ); - }, - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING))); - } else { - return TPromise.wrapError(new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Unable to install because, the depending extension '{0}' compatible with current version '{1}' of VS Code is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE)); - } - }, - error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); + compatible => { + if (compatible) { + this.logService.trace('Started downloading extension:', extension.name); + return this.galleryService.download(extension) + .then( + zipPath => { + this.logService.info('Downloaded extension:', extension.name); + return validateLocalExtension(zipPath) + .then( + manifest => ({ zipPath, id: getLocalExtensionIdFromManifest(manifest), metadata }), + error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_VALIDATING)) + ); + }, + error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_DOWNLOADING))); + } else { + return TPromise.wrapError(new ExtensionManagementError(nls.localize('notFoundCompatibleDependency', "Unable to install because, the depending extension '{0}' compatible with current version '{1}' of VS Code is not found.", extension.identifier.id, pkg.version), INSTALL_ERROR_INCOMPATIBLE)); + } + }, + error => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(error).message, INSTALL_ERROR_GALLERY))); } private onInstallExtensions(extensions: IGalleryExtension[]): void { @@ -377,18 +379,18 @@ export class ExtensionManagementService extends Disposable implements IExtension private installExtension(installableExtension: InstallableExtension): TPromise { return this.unsetUninstalledAndGetLocal(installableExtension.id) .then( - local => { - if (local) { - return local; - } - return this.extractAndInstall(installableExtension); - }, - e => { - if (isMacintosh) { - return TPromise.wrapError(new ExtensionManagementError(nls.localize('quitCode', "Unable to install the extension. Please Quit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED)); - } - return TPromise.wrapError(new ExtensionManagementError(nls.localize('exitCode', "Unable to install the extension. Please Exit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED)); - }); + local => { + if (local) { + return local; + } + return this.extractAndInstall(installableExtension); + }, + e => { + if (isMacintosh) { + return TPromise.wrapError(new ExtensionManagementError(nls.localize('quitCode', "Unable to install the extension. Please Quit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED)); + } + return TPromise.wrapError(new ExtensionManagementError(nls.localize('exitCode', "Unable to install the extension. Please Exit and Start VS Code before reinstalling."), INSTALL_ERROR_UNSET_UNINSTALLED)); + }); } private unsetUninstalledAndGetLocal(id: string): TPromise { @@ -426,23 +428,42 @@ export class ExtensionManagementService extends Disposable implements IExtension this.logService.trace(`Started extracting the extension from ${zipPath} to ${extractPath}`); return pfs.rimraf(extractPath) .then( - () => extract(zipPath, extractPath, options) - .then( - () => this.logService.info(`Extracted extension to ${extractPath}:`, id), - e => always(pfs.rimraf(extractPath), () => null) - .then(() => TPromise.wrapError(new ExtensionManagementError(e.message, INSTALL_ERROR_EXTRACTING)))), - e => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); + () => extract(zipPath, extractPath, options) + .then( + () => this.logService.info(`Extracted extension to ${extractPath}:`, id), + e => always(pfs.rimraf(extractPath), () => null) + .then(() => TPromise.wrapError(new ExtensionManagementError(e.message, INSTALL_ERROR_EXTRACTING)))), + e => TPromise.wrapError(new ExtensionManagementError(this.joinErrors(e).message, INSTALL_ERROR_DELETING))); } private completeInstall(id: string, extractPath: string): TPromise { - return pfs.rename(extractPath, path.join(this.extensionsPath, id)) + return this.renameWithRetry(id, extractPath) .then( - () => this.logService.info('Installation compelted.', id), - e => { - this.logService.info('Deleting the extracted extension', id); - return always(pfs.rimraf(extractPath), () => null) - .then(() => TPromise.wrapError(e)); - }); + () => this.logService.info('Installation compelted.', id), + e => { + this.logService.info('Deleting the extracted extension', id); + return always(pfs.rimraf(extractPath), () => null) + .then(() => TPromise.wrapError(e)); + }); + } + + private renameWithRetry(id: string, extractPath: string): TPromise { + const retry = (task: () => TPromise, shouldRetry: (err: any) => boolean) => { + return task().then( + null, + err => { + if (shouldRetry(err)) { + return retry(task, shouldRetry); + } else { + throw err; + } + }); + }; + + const retryUntil = Date.now() + ExtensionManagementService.RENAME_RETRY_TIME; + return retry( + () => pfs.rename(extractPath, path.join(this.extensionsPath, id)), + err => platform === Platform.Windows && Date.now() < retryUntil && err.code === 'EPERM'); } private rollback(extensions: IGalleryExtension[]): TPromise { @@ -519,10 +540,10 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.preUninstallExtension(extension) .then(() => this.hasDependencies(extension, installed) ? this.promptForDependenciesAndUninstall(extension, installed, force) : this.promptAndUninstall(extension, installed, force)) .then(() => this.postUninstallExtension(extension), - error => { - this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL); - return TPromise.wrapError(error); - }); + error => { + this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL); + return TPromise.wrapError(error); + }); } private hasDependencies(extension: ILocalExtension, installed: ILocalExtension[]): boolean { @@ -639,10 +660,10 @@ export class ExtensionManagementService extends Disposable implements IExtension return this.preUninstallExtension(extension) .then(() => this.uninstallExtension(extension)) .then(() => this.postUninstallExtension(extension), - error => { - this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL); - return TPromise.wrapError(error); - }); + error => { + this.postUninstallExtension(extension, INSTALL_ERROR_LOCAL); + return TPromise.wrapError(error); + }); } private preUninstallExtension(extension: ILocalExtension): TPromise { -- GitLab