From c8af09f62aba86ea24872097daaf4903d76d6577 Mon Sep 17 00:00:00 2001 From: Arzhan Kinzhalin Date: Sat, 30 May 2015 19:47:34 -0300 Subject: [PATCH] Support disabling/enabling extensions in UI. --- src/extensibility/ExtensionManager.js | 129 +++++++- src/extensibility/ExtensionManagerDialog.js | 148 +++++---- src/extensibility/ExtensionManagerView.js | 17 +- src/extensibility/Package.js | 59 +++- .../extension-manager-view-item.html | 13 + src/nls/root/strings.js | 9 +- test/spec/ExtensionManager-test.js | 294 +++++++++++++++++- 7 files changed, 588 insertions(+), 81 deletions(-) diff --git a/src/extensibility/ExtensionManager.js b/src/extensibility/ExtensionManager.js index b4335aabf6a..9c92eb4868f 100644 --- a/src/extensibility/ExtensionManager.js +++ b/src/extensibility/ExtensionManager.js @@ -104,8 +104,9 @@ define(function (require, exports, module) { /** * Requested changes to the installed extensions. */ - var _idsToRemove = [], - _idsToUpdate = []; + var _idsToRemove = {}, + _idsToUpdate = {}, + _idsToDisable = {}; PreferencesManager.stateManager.definePreference(FOLDER_AUTOINSTALL, "object", undefined); @@ -187,8 +188,9 @@ define(function (require, exports, module) { */ function _reset() { exports.extensions = extensions = {}; - _idsToRemove = []; - _idsToUpdate = []; + _idsToRemove = {}; + _idsToUpdate = {}; + _idsToDisable = {}; } /** @@ -267,7 +269,7 @@ define(function (require, exports, module) { metadata: metadata, path: path, locationType: locationType, - status: (e.type === "loadFailed" ? START_FAILED : (metadata.disabled ? DISABLED : ENABLED)) + status: (e.type === "loadFailed" ? START_FAILED : (e.type === "disabled" ? DISABLED : ENABLED)) }; synchronizeEntry(id); @@ -406,6 +408,60 @@ define(function (require, exports, module) { } return result.promise(); } + + /** + * Disables the installed extension with the given id. + * + * @param {string} id The id of the extension to disable. + * @return {$.Promise} A promise that's resolved when the extenion is disabled or + * rejected with an error that prevented the disabling. + */ + function disable(id) { + var result = new $.Deferred(), + extension = extensions[id]; + if (extension && extension.installInfo) { + Package.disable(extension.installInfo.path) + .done(function () { + extension.installInfo.status = DISABLED; + extension.installInfo.metadata.disabled = true; + result.resolve(); + exports.trigger("statusChange", id); + }) + .fail(function (err) { + result.reject(err); + }); + } else { + result.reject(StringUtils.format(Strings.EXTENSION_NOT_INSTALLED, id)); + } + return result.promise(); + } + + /** + * Enables the installed extension with the given id. + * + * @param {string} id The id of the extension to enable. + * @return {$.Promise} A promise that's resolved when the extenion is enabled or + * rejected with an error that prevented the enabling. + */ + function enable(id) { + var result = new $.Deferred(), + extension = extensions[id]; + if (extension && extension.installInfo) { + Package.enable(extension.installInfo.path) + .done(function () { + extension.installInfo.status = ENABLED; + extension.installInfo.metadata.disabled = false; + result.resolve(); + exports.trigger("statusChange", id); + }) + .fail(function (err) { + result.reject(err); + }); + } else { + result.reject(StringUtils.format(Strings.EXTENSION_NOT_INSTALLED, id)); + } + return result.promise(); + } /** * Updates an installed extension with the given package file. @@ -460,7 +516,7 @@ define(function (require, exports, module) { } exports.trigger("statusChange", id); } - + /** * Returns true if an extension is marked for removal. * @param {string} id The id of the extension to check. @@ -469,7 +525,7 @@ define(function (require, exports, module) { function isMarkedForRemoval(id) { return !!(_idsToRemove[id]); } - + /** * Returns true if there are any extensions marked for removal. * @return {boolean} true if there are extensions to remove @@ -478,6 +534,39 @@ define(function (require, exports, module) { return Object.keys(_idsToRemove).length > 0; } + /** + * Marks an extension for disabling later, or unmarks an extension previously marked. + * + * @param {string} id The id of the extension + * @param {boolean} mark Whether to mark or unmark the extension. + */ + function markForDisabling(id, mark) { + if (mark) { + _idsToDisable[id] = true; + } else { + delete _idsToDisable[id]; + } + exports.trigger("statusChange", id); + } + + /** + * Returns true if an extension is mark for disabling. + * + * @param {string} id The id of the extension to check. + * @return {boolean} true if it's been mark for disabling, false otherwise. + */ + function isMarkedForDisabling(id) { + return !!(_idsToDisable[id]); + } + + /** + * Returns true if there are any extensions marked for disabling. + * @return {boolean} true if there are extensions to disable + */ + function hasExtensionsToDisable() { + return Object.keys(_idsToDisable).length > 0; + } + /** * If a downloaded package appears to be an update, mark the extension for update. * If an extension was previously marked for removal, marking for update will @@ -550,6 +639,25 @@ define(function (require, exports, module) { } ); } + + /** + * Disables extensions marked for disabling. + * + * If the return promise is rejected, the argument will contain an array of objects. Each + * element is an object identifying the extension failed with "item" property set to the + * extension id which has failed to be disabled and "error" property set to the error. + * + * @return {$.Promise} A promise that's resolved when all extensions marked for disabling are + * disabled or rejected if one or more extensions can't be disabled. + */ + function disableMarkedExtensions() { + return Async.doInParallel_aggregateErrors( + Object.keys(_idsToDisable), + function (id) { + return disable(id); + } + ); + } /** * Updates extensions previously marked for update. @@ -784,17 +892,23 @@ define(function (require, exports, module) { exports.getExtensionURL = getExtensionURL; exports.remove = remove; exports.update = update; + exports.disable = disable; + exports.enable = enable; exports.extensions = extensions; exports.cleanupUpdates = cleanupUpdates; exports.markForRemoval = markForRemoval; exports.isMarkedForRemoval = isMarkedForRemoval; exports.unmarkAllForRemoval = unmarkAllForRemoval; exports.hasExtensionsToRemove = hasExtensionsToRemove; + exports.markForDisabling = markForDisabling; + exports.isMarkedForDisabling = isMarkedForDisabling; + exports.hasExtensionsToDisable = hasExtensionsToDisable; exports.updateFromDownload = updateFromDownload; exports.removeUpdate = removeUpdate; exports.isMarkedForUpdate = isMarkedForUpdate; exports.hasExtensionsToUpdate = hasExtensionsToUpdate; exports.removeMarkedExtensions = removeMarkedExtensions; + exports.disableMarkedExtensions = disableMarkedExtensions; exports.updateExtensions = updateExtensions; exports.getAvailableUpdates = getAvailableUpdates; exports.cleanAvailableUpdates = cleanAvailableUpdates; @@ -802,6 +916,7 @@ define(function (require, exports, module) { exports.hasDownloadedRegistry = false; exports.ENABLED = ENABLED; + exports.DISABLED = DISABLED; exports.START_FAILED = START_FAILED; exports.LOCATION_DEFAULT = LOCATION_DEFAULT; diff --git a/src/extensibility/ExtensionManagerDialog.js b/src/extensibility/ExtensionManagerDialog.js index b0a6367bf6b..6b7f1d802d6 100644 --- a/src/extensibility/ExtensionManagerDialog.js +++ b/src/extensibility/ExtensionManagerDialog.js @@ -63,17 +63,20 @@ define(function (require, exports, module) { */ function _performChanges() { // If an extension was removed or updated, prompt the user to quit Brackets. - var hasRemovedExtensions = ExtensionManager.hasExtensionsToRemove(), - hasUpdatedExtensions = ExtensionManager.hasExtensionsToUpdate(); - if (!hasRemovedExtensions && !hasUpdatedExtensions) { + var hasRemovedExtensions = ExtensionManager.hasExtensionsToRemove(), + hasUpdatedExtensions = ExtensionManager.hasExtensionsToUpdate(), + hasDisabledExtensions = ExtensionManager.hasExtensionsToDisable(); + if (!hasRemovedExtensions && !hasUpdatedExtensions && !hasDisabledExtensions) { return; } var buttonLabel = Strings.CHANGE_AND_RELOAD; - if (hasRemovedExtensions && !hasUpdatedExtensions) { + if (hasRemovedExtensions && !hasUpdatedExtensions && !hasDisabledExtensions) { buttonLabel = Strings.REMOVE_AND_RELOAD; - } else if (hasUpdatedExtensions && !hasRemovedExtensions) { + } else if (hasUpdatedExtensions && !hasRemovedExtensions && !hasDisabledExtensions) { buttonLabel = Strings.UPDATE_AND_RELOAD; + } else if (hasDisabledExtensions && !hasRemovedExtensions && !hasUpdatedExtensions) { + buttonLabel = Strings.DISABLE_AND_RELOAD; } var dlg = Dialogs.showModalDialog( @@ -107,57 +110,98 @@ define(function (require, exports, module) { .text(Strings.PROCESSING_EXTENSIONS) .append(""); - ExtensionManager.removeMarkedExtensions() + var removeExtensionsPromise, + updateExtensionsPromise, + disableExtensionsPromise, + removeErrors, + updateErrors, + disableErrors; + + removeExtensionsPromise = ExtensionManager.removeMarkedExtensions(); + removeExtensionsPromise + .fail(function (errorArray) { + removeErrors = errorArray; + }); + updateExtensionsPromise = ExtensionManager.updateExtensions(); + updateExtensionsPromise + .fail(function (errorArray) { + updateErrors = errorArray; + }); + disableExtensionsPromise = ExtensionManager.disableMarkedExtensions(); + disableExtensionsPromise + .fail(function (errorArray) { + disableErrors = errorArray; + }); + + Async.waitForAll([removeExtensionsPromise, updateExtensionsPromise, disableExtensionsPromise]) + .always(function () { + dlg.close(); + }) .done(function () { - ExtensionManager.updateExtensions() - .done(function () { - dlg.close(); + CommandManager.execute(Commands.APP_RELOAD); + }) + .fail(function () { + var ids = [], + dialogs = []; + + function nextDialog() { + var dialog = dialogs.shift(); + if (dialog) { + Dialogs.showModalDialog(dialog.dialog, dialog.title, dialog.message) + .done(nextDialog); + } else { + // Even in case of error condition, we still have to reload CommandManager.execute(Commands.APP_RELOAD); - }) - .fail(function (errorArray) { - dlg.close(); - - // This error case should be very uncommon. - // Just let the user know that we couldn't update - // this extension and log the errors to the console. - var ids = []; - errorArray.forEach(function (errorObj) { - ids.push(errorObj.item); - if (errorObj.error && errorObj.error.forEach) { - console.error("Errors for", errorObj.item); - errorObj.error.forEach(function (error) { - console.error(Package.formatError(error)); - }); - } else { - console.error("Error for", errorObj.item, errorObj); - } - }); - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_ERROR, - Strings.EXTENSION_MANAGER_UPDATE, - StringUtils.format(Strings.EXTENSION_MANAGER_UPDATE_ERROR, ids.join(", ")) - ).done(function () { - // We still have to reload even if some of the removals failed. - CommandManager.execute(Commands.APP_RELOAD); - }); + } + } + + if (removeErrors) { + removeErrors.forEach(function (errorObj) { + ids.push(errorObj.item); }); - }) - .fail(function (errorArray) { - dlg.close(); - ExtensionManager.cleanupUpdates(); + dialogs.push({ + dialog: DefaultDialogs.DIALOG_ID_ERROR, + title: Strings.EXTENSION_MANAGER_REMOVE, + message: StringUtils.format(Strings.EXTENSION_MANAGER_REMOVE_ERROR, ids.join(", ")) + }); + } - var ids = []; - errorArray.forEach(function (errorObj) { - ids.push(errorObj.item); - }); - Dialogs.showModalDialog( - DefaultDialogs.DIALOG_ID_ERROR, - Strings.EXTENSION_MANAGER_REMOVE, - StringUtils.format(Strings.EXTENSION_MANAGER_REMOVE_ERROR, ids.join(", ")) - ).done(function () { - // We still have to reload even if some of the removals failed. - CommandManager.execute(Commands.APP_RELOAD); - }); + if (updateErrors) { + // This error case should be very uncommon. + // Just let the user know that we couldn't update + // this extension and log the errors to the console. + ids.length = 0; + updateErrors.forEach(function (errorObj) { + ids.push(errorObj.item); + if (errorObj.error && errorObj.error.forEach) { + console.error("Errors for", errorObj.item); + errorObj.error.forEach(function (error) { + console.error(Package.formatError(error)); + }); + } else { + console.error("Error for", errorObj.item, errorObj); + } + }); + dialogs.push({ + dialog: DefaultDialogs.DIALOG_ID_ERROR, + title: Strings.EXTENSION_MANAGER_UPDATE, + message: StringUtils.format(Strings.EXTENSION_MANAGER_UPDATE_ERROR, ids.join(", ")) + }); + } + + if (disableErrors) { + ids.length = 0; + disableErrors.forEach(function (errorObj) { + ids.push(errorObj.item); + }); + dialogs.push({ + dialog: DefaultDialogs.DIALOG_ID_ERROR, + title: Strings.EXTENSION_MANAGER_DISABLE, + message: StringUtils.format(Strings.EXTENSION_MANAGER_DISABLE_ERROR, ids.join(", ")) + }); + } + + nextDialog(); }); } else { dlg.close(); diff --git a/src/extensibility/ExtensionManagerView.js b/src/extensibility/ExtensionManagerView.js index b6781a4a42d..00640258845 100644 --- a/src/extensibility/ExtensionManagerView.js +++ b/src/extensibility/ExtensionManagerView.js @@ -181,6 +181,8 @@ define(function (require, exports, module) { ExtensionManager.markForRemoval($target.attr("data-extension-id"), true); } else if ($target.hasClass("undo-update")) { ExtensionManager.removeUpdate($target.attr("data-extension-id")); + } else if ($target.hasClass("undo-disable")) { + ExtensionManager.markForDisabling($target.attr("data-extension-id"), false); } else if ($target.data("toggle-desc") === "expand-desc") { this._toggleDescription($target.attr("data-extension-id"), $target, true); } else if ($target.data("toggle-desc") === "trunc-desc") { @@ -195,6 +197,12 @@ define(function (require, exports, module) { }) .on("click", "button.remove", function (e) { ExtensionManager.markForRemoval($(e.target).attr("data-extension-id"), true); + }) + .on("click", "button.disable", function (e) { + ExtensionManager.markForDisabling($(e.target).attr("data-extension-id"), true); + }) + .on("click", "button.enable", function (e) { + ExtensionManager.enable($(e.target).attr("data-extension-id")); }); }; @@ -221,6 +229,7 @@ define(function (require, exports, module) { // arrays as iteration contexts. context.isInstalled = !!entry.installInfo; context.failedToStart = (entry.installInfo && entry.installInfo.status === ExtensionManager.START_FAILED); + context.disabled = (entry.installInfo && entry.installInfo.status === ExtensionManager.DISABLED); context.hasVersionInfo = !!info.versions; if (entry.registryInfo) { @@ -259,7 +268,9 @@ define(function (require, exports, module) { } context.isMarkedForRemoval = ExtensionManager.isMarkedForRemoval(info.metadata.name); + context.isMarkedForDisabling = ExtensionManager.isMarkedForDisabling(info.metadata.name); context.isMarkedForUpdate = ExtensionManager.isMarkedForUpdate(info.metadata.name); + var hasPendingAction = context.isMarkedForDisabling || context.isMarkedForRemoval || context.isMarkedForUpdate; context.showInstallButton = (this.model.source === this.model.SOURCE_REGISTRY || this.model.source === this.model.SOURCE_THEMES) && !context.updateAvailable; context.showUpdateButton = context.updateAvailable && !context.isMarkedForUpdate && !context.isMarkedForRemoval; @@ -314,7 +325,11 @@ define(function (require, exports, module) { } context.removalAllowed = this.model.source === "installed" && - !context.failedToStart && !context.isMarkedForUpdate && !context.isMarkedForRemoval; + !context.failedToStart && !hasPendingAction; + context.disablingAllowed = this.model.source === "installed" && + !context.disabled && !hasPendingAction; + context.enablingAllowed = this.model.source === "installed" && + context.disabled && !hasPendingAction; // Copy over helper functions that we share with the registry app. ["lastVersionDate", "authorInfo"].forEach(function (helper) { diff --git a/src/extensibility/Package.js b/src/extensibility/Package.js index 6cee87b062c..cc4025f04d0 100644 --- a/src/extensibility/Package.js +++ b/src/extensibility/Package.js @@ -430,6 +430,47 @@ define(function (require, exports, module) { }); } + /** + * Disables the extension at the given path. + * + * @param {string} path The absolute path to the extension to disable. + * @return {$.Promise} A promise that's resolved when the extenion is disabled, or + * rejected if there was an error. + */ + function disable(path) { + var result = new $.Deferred(), + file = FileSystem.getFileForPath(path + "/.disabled"); + file.write("", function (err) { + if (err) { + result.reject(err); + } else { + result.resolve(); + } + }); + return result.promise(); + } + + /** + * Enables the extension at the given path. + * + * @param {string} path The absolute path to the extension to enable. + * @return {$.Promise} A promise that's resolved when the extenion is enable, or + * rejected if there was an error. + */ + function enable(path) { + var result = new $.Deferred(), + file = FileSystem.getFileForPath(path + "/.disabled"); + file.unlink(function (err) { + if (err) { + result.reject(err); + return; + } + ExtensionLoader.loadExtension(FileUtils.getBaseName(path), { baseUrl: path }, "main") + .done(result.resolve) + .fail(result.reject); + }); + } + /** * Install an extension update located at path. * This assumes that the installation was previously attempted @@ -498,12 +539,14 @@ define(function (require, exports, module) { // For unit tests only exports._getNodeConnectionDeferred = _getNodeConnectionDeferred; - exports.installFromURL = installFromURL; - exports.installFromPath = installFromPath; - exports.validate = validate; - exports.install = install; - exports.remove = remove; - exports.installUpdate = installUpdate; - exports.formatError = formatError; - exports.InstallationStatuses = InstallationStatuses; + exports.installFromURL = installFromURL; + exports.installFromPath = installFromPath; + exports.validate = validate; + exports.install = install; + exports.remove = remove; + exports.disable = disable; + exports.enable = enable; + exports.installUpdate = installUpdate; + exports.formatError = formatError; + exports.InstallationStatuses = InstallationStatuses; }); diff --git a/src/htmlContent/extension-manager-view-item.html b/src/htmlContent/extension-manager-view-item.html index c966f681cb3..40e25169475 100644 --- a/src/htmlContent/extension-manager-view-item.html +++ b/src/htmlContent/extension-manager-view-item.html @@ -63,6 +63,16 @@ {{Strings.UPDATE}} {{/showUpdateButton}} + {{#disablingAllowed}} + + {{/disablingAllowed}} + {{#enablingAllowed}} + + {{/enablingAllowed}} {{#removalAllowed}}