Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

AutoUpdate Framework #14177

Merged
merged 16 commits into from
Apr 18, 2018
Merged

AutoUpdate Framework #14177

merged 16 commits into from
Apr 18, 2018

Conversation

mbhavi
Copy link
Contributor

@mbhavi mbhavi commented Mar 27, 2018

This PR includes changes for Auto Update of Brackets.
The changes are with respect to:

  1. Windows implementation
  2. Mac implementation
  3. A new UI

ping @swmitra @nethip for review

The Auto Update workflow along with the screenshots is described below :

  • Notification of Update : through current UpdateNotification dialog, which is triggered either from Help->CheckForUpdates or from the side bar button notification

image

  • Download of Update : Once user clicks on GetItNow button, a download of the latest installer is triggered in the background, with a progress bar shown on the status bar.

image

  • Download retrial in case of failure : If the download fails, it is retried for a maximum of 5 attempts, and the same is reflected on the status bar UI.

image

  • Validation of the downloaded update : Once download is successful, the SHA256 checksum is validated, and the info is displayed on status bar.

image

  • Download Failure : In case download fails, error is shown to the user towards bottom of the editor.

image

  • Download and Validation success : User is prompted with the update bar, with buttons for "Restart" and "Later" options. Clicking Restart button would start the update procedure by quitting the app, and launching the installer. Clicking on Later button allows user to continue with the normal flow of Brackets.

image

  • Validation Failure : If the validation of the downloaded update fails, a validation error is displayed to the user in update bar.

image

  • Update in Progress : Once the user clicks Restart button, the app is quit and installer gets launched.

image

  • Update Success : If update is successful, new version of Brackets is launched automatically, with the success message on the update bar.

image

  • Update Failure : If the update fails, the older version of Brackets is relaunched automatically, and the error message is displayed to the user in update bar, with a link to the brackets website for a manual update operation.

image

Any warnings or errors encountered during the worflow are reflected in the update bar.
Screenshots for one of the warnings is attached here. It corresponds to the scenario where user clicked on Restart button, but had dirty files to which he/she chose Cancel in File Save dialog. In this case, the update will happen on the next quit of Brackets, and the same is displayed to the user as a warning on update bar.

image

On MAC, instead of the installer, the update procedure is carried out by an update shell script. Rest everything is same for MAC, as above.

@mbhavi mbhavi changed the title [WIP] AutoUpdate Framework : Initial Changes [WIP] AutoUpdate Framework Mar 27, 2018
@vickramdhawal vickramdhawal requested a review from nethip March 28, 2018 11:26
@abose
Copy link
Contributor

abose commented Mar 31, 2018

Awesome! @mbhavi Can you add some screenshots of the new flow?

@abose
Copy link
Contributor

abose commented Apr 2, 2018

Is there a preference to disable auto updates?

@mbhavi
Copy link
Contributor Author

mbhavi commented Apr 3, 2018

@abose , there isn't a preference to disable auto updates.
Going forward, auto update will be the default update mechanism in Brackets.
The feature is implemented as an extension AutoUpdate, which if not present or not loaded successfully, the update mechanism will fallback to the current manual update wherein if the user clicks on GetItNow button in UpdateNotification dialog, he/she is redirected to brackets website for a manual update.

@abose
Copy link
Contributor

abose commented Apr 3, 2018

Sometimes, users disable auto-updates to prevent breaking changes to their setup[extension compatibilities Et.Al.]. It would be good if it is configurable by a preference without having to remove the extension.
Also is there an opt-in at first launch? or is the update process kicked off without confirmation from the user?
@petetnt @swmitra

@mbhavi
Copy link
Contributor Author

mbhavi commented Apr 3, 2018

@abose, I have attached the screenshots and the step-by-step workflow description.
The update is kicked off only after the confirmation from the user as an input of GetItNow button click in UpdateNotification dialog.

var DIRTY_FILESAVE_CANCELLED = "dirtyFileSaveCancelled";

/**
Event triggered when error is returned when tried to check if Auto Update is progress
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: trying instead of tried.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in 537430b

* Checks and handles if auto update is currently in progress
*/
function checkIfAutoUpdateInProgress() {
brackets.app.isAutoUpdateInProgress(function (err, isAutoUpdateInProgress) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add if (brackets.app.isAutoUpdateInProgress) { to check if this function is registered or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

isAutoUpdateInProgress() function has been added to the core. It is not a registered function. Hence, we need not have this check.

Copy link
Contributor

@nethip nethip Apr 9, 2018

Choose a reason for hiding this comment

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

@mbhavi It is a registered in appshell_extensions.js. Consider this scenario where a user who is running the older shell but with newer brackets code, will never find this function, so it is a good idea to bailout. Please add a check here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I strongly believe suppressing the error would turn out to be harmful in this case, not showing the error to the user would be misleading to him/her believing that the feature can work with just the brackets changes.
Moreover, I don't see that with any of the other functions' invocations. I believe doing that will be unnecessary bloating of the code, as this is most likely to happen only in the dev machines, which I think should be okay.

*/
function checkIfAutoUpdateInProgress() {
brackets.app.isAutoUpdateInProgress(function (err, isAutoUpdateInProgress) {
if (err === 0 && isAutoUpdateInProgress) {
Copy link
Contributor

@nethip nethip Apr 5, 2018

Choose a reason for hiding this comment

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

No need for an explicit check, general rule we follow in Brackets is to check for error condition first. So your statements should be


if (err) {
 
} else {

}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in 537430b

function checkIfAutoUpdateInProgress() {
brackets.app.isAutoUpdateInProgress(function (err, isAutoUpdateInProgress) {
if (err === 0 && isAutoUpdateInProgress) {
CommandManager.trigger(exports.DIRTY_FILESAVE_CANCELLED);
Copy link
Contributor

Choose a reason for hiding this comment

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

Use execute over trigger.

Copy link
Contributor

Choose a reason for hiding this comment

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

Help me understand. The name of the method is checkIfAutoUpdateInProgress but you are executing exports.DIRTY_FILESAVE_CANCELLED ? Shouldn't this be something like whoever is calling this method, should be returned a promise and the client can then act upon the promise state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have now placed all the events related to Auto Update, needed in core, in UpdateNotification in order to ensure consistency.
Also, I now chose a better name for checkIfAutoUpdateProgress(). It is actually a handler function. It iinternally calls a function isAutoUpdateInProgress(), which returns a bool, and the wrapper function handles this bool. So, I have renamed the wrapper function to handleIfAutoUpdateInProgress().
Please refer to 537430b

@@ -996,6 +1020,7 @@ define(function (require, exports, module) {
if (selectedPath) {
_doSaveAfterSaveDialog(selectedPath);
} else {
checkIfAutoUpdateInProgress();
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you explain (workflow) why this call is required here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed for the workflow where user clicked Restart button for the update to happen, but had some dirty files, because of which the app could not be quit, and instead, he got the Unsaved Files dialog, This dialog has 3 buttons : Save/Don't Save/Cancel. If Don't Save is pressed, we quit the app and run the update. If Save is pressed and user saves the file successfully, then also we quit the app and update. But when the user chooses to either press Cancel button in the above dialog, or presses Save followed by Cancel in the Save As dialog, we show him a message saying that the update will now be reflected on relaunch, which inherently means that the update will now be carried out the next time the app quits.
So, for this, we need to know whether or not the save file was cancelled by either of the two scenarios mentioned here, so that we can prompt him with the required message.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for explanation @mbhavi

@@ -1217,6 +1242,7 @@ define(function (require, exports, module) {
)
.done(function (id) {
if (id === Dialogs.DIALOG_BTN_CANCEL) {
checkIfAutoUpdateInProgress();
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like most of the auto update code is written as an extension. In that case, you should only send notifications about the relevant events here and let the extension react to those notifications.

Eg: You should send a notification which says save cancelled/file save failed e.t.c.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am indeed sending the notification of a Cancel click on dirty file save operation. Please refer to my comments above.

_file = FileSystem.getFileForPath(this.path);

_file.exists(function (err, exists) {
if (exists) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Check for err.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 537430b

*/
function _write(filePath, json) {
var result = $.Deferred(),
_file = FileSystem.getFileForPath(filePath);
Copy link
Contributor

Choose a reason for hiding this comment

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

check for validity of _file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_write is an internal function, which I have wrapped inside the function writePromise(), which in turn, is wrapped inside the write() function, which checks for the validity of the file. Hence, checking here is redundant.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think there is not harm in checking for validity of _file given the fact that you are going to stringify the JSON irrespective of whether _file is valid or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nethip , the requirement here is to do a force write. If the file exists, the content is overwritten. If it doesn't exist, the file is created and written. I am passing the boolean to be "true" in writeText, which ensures this.
Hence, the validity is not needed here.

Copy link
Contributor

Choose a reason for hiding this comment

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

@mbhavi Let us not have discussion of having a null check or not. It is a simple null check. If you still feel that is not required, post merging this PR, one of us will add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nethip , addressed in 4fb6582

});

$restart.keyup(function (e) {
if (e.which === 32) { //32 is the keycode for space key
Copy link
Contributor

Choose a reason for hiding this comment

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

use a named variable for better readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 537430b

updateDir = appSupportDirectory + '/updateTemp',
updateJsonPath = updateDir + '/' + 'updateHelper.json';

var updateJsonHandler = new StateHandler.StateHandler(updateJsonPath);
Copy link
Contributor

Choose a reason for hiding this comment

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

StateHandler.StateHandler?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 537430b


/**
* Checks if it is the first iteration of download
* @returns {boolean} - true if first iteration, false if is a retrial of download
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: if it is a

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 537430b

* to be called in case of
* successful setting of update state
*/
function setUpdateStateInJSON(key, value, callback) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a promise instead of a callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is indeed a promise based function. The same promise handling code snippet was getting used at multiple places, and so, in order to avoid the repetition of code, I have templatized the code snippet.

*/
function handleSafeToDownload() {

var downloadCB = function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Use promises rather than callbacks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please refer to my comment above.

functionMap["brackets.registerBracketsFunctions"] = registerBracketsFunctions;

UpdateNotification.on(UpdateNotification.GET_AUTOUPDATE_INSTALLER, handleGetNow);
CommandManager.on(DocumentCommandHandlers.DIRTY_FILESAVE_CANCELLED, dirtyFileSaveCancelled);
Copy link
Contributor

Choose a reason for hiding this comment

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

Refer to the comment that I put above in DocumentCommandHandler about generic command handlers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Please refer to my comments in DocumentCommandHandlers and UpdateNotification, where I have explained the use cases and requirements.

});
updateDomain.on('data', receiveMessageFromNode);

AppInit.appReady(function () {
Copy link
Contributor

Choose a reason for hiding this comment

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

Check for platform and only then initialize AppInit.appReady as auto update is targeted for only Win and Mac.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 537430b

UpdateInfoBar = require("UpdateInfoBar");


var _modulePath = FileUtils.getNativeModuleDirectoryPath(module),
Copy link
Contributor

Choose a reason for hiding this comment

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

Execute the steps only for Win and Mac

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this in 537430b

[]
);

domainManager.registerEvent(
Copy link
Contributor

Choose a reason for hiding this comment

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

Empty function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is actually an event registration, for which no naming is required.

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it! Thanks

@mbhavi
Copy link
Contributor Author

mbhavi commented Apr 9, 2018

@nethip , I have addressed and committed all the required changes. Also, I have replied to all the comments.
The PR is ready for review again.

@nethip
Copy link
Contributor

nethip commented Apr 9, 2018

@mbhavi I think the preference to disable is still pending.

@nethip
Copy link
Contributor

nethip commented Apr 9, 2018

@mbhavi I don't think the changes which I had proposed with DocumentCommandHandler.js are incorporated.

There should be no reference of AutoUpdate anywhere inside DocumentCommandHandler.js. You could still borrow the mechanism but no reference to auto update. I still see methods like handleIfAutoUpdateInProgress or in the PR. I am not sure if there is any any strong reason for hard wiring auto update into DocumentCommandHandler.js, as you could create new events and emit those events from here. The AutoUpdate extension can then register for those events and react accordingly..

UpdateNotification.on(UpdateNotification.DIRTY_FILESAVE_CANCELLED, dirtyFileSaveCancelled);
UpdateNotification.on(UpdateNotification.AUTOUPDATE_ERROR, handleAutoUpdateError);

if(brackets.platform !== "linux") {
Copy link
Contributor

Choose a reason for hiding this comment

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

How about checking for the shell functions existence like setUpdateParamsAndQuit, and if not present, just bail out?

@@ -333,6 +447,12 @@ define(function (require, exports, module) {
var result = new $.Deferred();
var versionInfoUrl;

//AUTOUPDATE_UNITTESTING
var updateJSONLocalhostURL = PreferencesManager.get("updateJSONLocalhostURL");
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be removed.


//If no checksum field is present then we're setting it to 0, just as a safety check,
// although ideally this situation should never occur in releases post its introduction.
var platformInfo = getPlatformInfo(),
Copy link
Contributor

Choose a reason for hiding this comment

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

If you want to retain this , you might want to move all of this to notifyGetUpdateInstaller

@mbhavi mbhavi force-pushed the miglani/autoUpdate branch from 801a51e to bcb2a3e Compare April 10, 2018 09:03
…date, making AutoUpdate no-op for Linux, Replacing icons
@mbhavi
Copy link
Contributor Author

mbhavi commented Apr 10, 2018

@nethip , the review comments have been addressed now. I have added the preference to enable/disable auto update, made AutoUpdate no-op on Linux, replaced the icons, and moved most of the core code onto extension side. Please find the changes in b50f8de
The changes are ready to be reviewed again.

@@ -61,6 +71,10 @@ define(function (require, exports, module) {
// Data about available updates in the registry
PreferencesManager.stateManager.definePreference("extensionUpdateInfo", "Array", []);

//Preference for hosting latest installer on a local server
//AUTOUPDATE_UNITTESTING
PreferencesManager.stateManager.definePreference("autoUpdate.testUrl", "string", "");
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to still retain this preference?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nethip , Yes we need to retain this preference, as this will be very much needed for unit testing purposes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please have this preference in a separate branch. We can't be shipping with test URLs preference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nethip, I have removed the preference from this branch. I will probably include the changes in a separate branch then, for unit testing purposes.

AppInit.appReady(function () {

// Auto Update is supported on Win and Mac, as of now
if (brackets.platform !== "linux") {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think one of the review comments got missed here. You need to check if brackets.app.setUpdateParams is registered or not. Only then run through the flow.

@nethip
Copy link
Contributor

nethip commented Apr 10, 2018

Thanks for addressing the review comments @mbhavi . I have added more comments. Please address those comments and I think we are good to merge from the dev perspective.

@mbhavi
Copy link
Contributor Author

mbhavi commented Apr 10, 2018

@nethip, I have addressed the new comments in 4fb6582. Please review.

@nethip
Copy link
Contributor

nethip commented Apr 11, 2018

LGTM.

@mbhavi Thanks for this hugely requested PR 😄 ! Great job 💯

@ayushrathi15 We are waiting for the test results from your sanity testing. Please update this thread once you are done testing the private builds, so that we can go ahead with the merge.

@nethip
Copy link
Contributor

nethip commented Apr 13, 2018

@mbhavi One last check. What if the node domain fails? Can we figure that out and bail out in your extension?

@abose abose mentioned this pull request Apr 17, 2018
@mbhavi
Copy link
Contributor Author

mbhavi commented Apr 17, 2018

@nethip , I have addressed the failure scenario where the node domain fails in b272289. Please have a quick review for the same.

@ayushrathi15
Copy link
Collaborator

LGTM.

@nethip @mbhavi I am done with the private build testing. We are good to go ahead with the merge.

@mbhavi mbhavi changed the title [WIP] AutoUpdate Framework AutoUpdate Framework Apr 17, 2018
}
})
.fail(function() {
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

A console message message here would be good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nethip, addressed this in 9b89697.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants