From 92350e421df12fd727d3e12c503f09e344e26478 Mon Sep 17 00:00:00 2001 From: ed Date: Wed, 6 Mar 2019 17:08:30 +0000 Subject: [PATCH] Projucer: Refactor the autoupdater to set executable file permissions after updating --- .../Source/Application/jucer_Application.cpp | 12 +- .../Source/Application/jucer_Application.h | 2 - .../Source/Application/jucer_AutoUpdater.cpp | 1003 ++++++----------- .../Source/Application/jucer_AutoUpdater.h | 82 +- .../Source/Application/jucer_CommandIDs.h | 1 + .../Source/Utility/Helpers/jucer_PresetIDs.h | 1 + 6 files changed, 389 insertions(+), 712 deletions(-) diff --git a/extras/Projucer/Source/Application/jucer_Application.cpp b/extras/Projucer/Source/Application/jucer_Application.cpp index 520cf02809..4c11fb4609 100644 --- a/extras/Projucer/Source/Application/jucer_Application.cpp +++ b/extras/Projucer/Source/Application/jucer_Application.cpp @@ -174,7 +174,8 @@ void ProjucerApplication::handleAsyncUpdate() MenuBarModel::setMacMainMenu (menuModel.get(), &extraAppleMenuItems); //, "Open Recent"); #endif - versionChecker.reset (new LatestVersionChecker()); + if (getGlobalProperties().getValue (Ids::dontQueryForUpdate, {}).isEmpty()) + LatestVersionCheckerAndUpdater::getInstance()->checkForNewVersion (false); if (licenseController != nullptr) { @@ -217,7 +218,6 @@ void ProjucerApplication::shutdown() Logger::writeToLog ("Server shutdown cleanly"); } - versionChecker.reset(); utf8Window.reset(); svgPathWindow.reset(); aboutWindow.reset(); @@ -438,6 +438,7 @@ void ProjucerApplication::createFileMenu (PopupMenu& menu) #if ! JUCE_MAC menu.addCommandItem (commandManager.get(), CommandIDs::showAboutWindow); menu.addCommandItem (commandManager.get(), CommandIDs::showAppUsageWindow); + menu.addCommandItem (commandManager.get(), CommandIDs::checkForNewVersion); menu.addCommandItem (commandManager.get(), CommandIDs::showGlobalPathsWindow); menu.addSeparator(); menu.addCommandItem (commandManager.get(), StandardApplicationCommandIDs::quit); @@ -589,6 +590,7 @@ void ProjucerApplication::createExtraAppleMenuItems (PopupMenu& menu) { menu.addCommandItem (commandManager.get(), CommandIDs::showAboutWindow); menu.addCommandItem (commandManager.get(), CommandIDs::showAppUsageWindow); + menu.addCommandItem (commandManager.get(), CommandIDs::checkForNewVersion); menu.addSeparator(); menu.addCommandItem (commandManager.get(), CommandIDs::showGlobalPathsWindow); } @@ -1000,6 +1002,7 @@ void ProjucerApplication::getAllCommands (Array & commands) CommandIDs::showSVGPathTool, CommandIDs::showAboutWindow, CommandIDs::showAppUsageWindow, + CommandIDs::checkForNewVersion, CommandIDs::showForum, CommandIDs::showAPIModules, CommandIDs::showAPIClasses, @@ -1090,6 +1093,10 @@ void ProjucerApplication::getCommandInfo (CommandID commandID, ApplicationComman result.setInfo ("Application Usage Data", "Shows the application usage data agreement window", CommandCategories::general, 0); break; + case CommandIDs::checkForNewVersion: + result.setInfo ("Check for New Version...", "Checks the web server for a new version of JUCE", CommandCategories::general, 0); + break; + case CommandIDs::showForum: result.setInfo ("JUCE Community Forum", "Shows the JUCE community forum in a browser", CommandCategories::general, 0); break; @@ -1149,6 +1156,7 @@ bool ProjucerApplication::perform (const InvocationInfo& info) case CommandIDs::showGlobalPathsWindow: showPathsWindow (false); break; case CommandIDs::showAboutWindow: showAboutWindow(); break; case CommandIDs::showAppUsageWindow: showApplicationUsageDataAgreementPopup(); break; + case CommandIDs::checkForNewVersion: LatestVersionCheckerAndUpdater::getInstance()->checkForNewVersion (true); break; case CommandIDs::showForum: launchForumBrowser(); break; case CommandIDs::showAPIModules: launchModulesBrowser(); break; case CommandIDs::showAPIClasses: launchClassesBrowser(); break; diff --git a/extras/Projucer/Source/Application/jucer_Application.h b/extras/Projucer/Source/Application/jucer_Application.h index 7a7c5786c5..9b5ddc38b4 100644 --- a/extras/Projucer/Source/Application/jucer_Application.h +++ b/extras/Projucer/Source/Application/jucer_Application.h @@ -123,7 +123,6 @@ public: void launchTutorialsBrowser(); void updateAllBuildTabs(); - LatestVersionChecker* createVersionChecker() const; //============================================================================== void licenseStateChanged (const LicenseState&) override; @@ -196,7 +195,6 @@ private: //============================================================================== void* server = nullptr; - std::unique_ptr versionChecker; TooltipWindow tooltipWindow; AvailableModuleList jucePathModuleList, userPathsModuleList; diff --git a/extras/Projucer/Source/Application/jucer_AutoUpdater.cpp b/extras/Projucer/Source/Application/jucer_AutoUpdater.cpp index 2cfc53a275..2fe10e8e37 100644 --- a/extras/Projucer/Source/Application/jucer_AutoUpdater.cpp +++ b/extras/Projucer/Source/Application/jucer_AutoUpdater.cpp @@ -28,808 +28,523 @@ #include "jucer_Application.h" #include "jucer_AutoUpdater.h" -LatestVersionChecker::JuceVersionTriple::JuceVersionTriple() - : major ((ProjectInfo::versionNumber & 0xff0000) >> 16), - minor ((ProjectInfo::versionNumber & 0x00ff00) >> 8), - build ((ProjectInfo::versionNumber & 0x0000ff) >> 0) -{} - -LatestVersionChecker::JuceVersionTriple::JuceVersionTriple (int juceVersionNumber) - : major ((juceVersionNumber & 0xff0000) >> 16), - minor ((juceVersionNumber & 0x00ff00) >> 8), - build ((juceVersionNumber & 0x0000ff) >> 0) -{} - -LatestVersionChecker::JuceVersionTriple::JuceVersionTriple (int majorInt, int minorInt, int buildNumber) - : major (majorInt), - minor (minorInt), - build (buildNumber) -{} - -bool LatestVersionChecker::JuceVersionTriple::fromString (const String& versionString, - LatestVersionChecker::JuceVersionTriple& result) +//============================================================================== +LatestVersionCheckerAndUpdater::LatestVersionCheckerAndUpdater() + : Thread ("VersionChecker") { - StringArray tokenizedString = StringArray::fromTokens (versionString, ".", StringRef()); - - if (tokenizedString.size() != 3) - return false; - - result.major = tokenizedString [0].getIntValue(); - result.minor = tokenizedString [1].getIntValue(); - result.build = tokenizedString [2].getIntValue(); - - return true; } -String LatestVersionChecker::JuceVersionTriple::toString() const +LatestVersionCheckerAndUpdater::~LatestVersionCheckerAndUpdater() { - String retval; - retval << major << '.' << minor << '.' << build; - return retval; + stopThread (1000); + clearSingletonInstance(); } -bool LatestVersionChecker::JuceVersionTriple::operator> (const LatestVersionChecker::JuceVersionTriple& b) const noexcept +void LatestVersionCheckerAndUpdater::checkForNewVersion (bool showAlerts) { - if (major == b.major) + if (! isThreadRunning()) { - if (minor == b.minor) - return build > b.build; - - return minor > b.minor; + showAlertWindows = showAlerts; + startThread (3); } +} + +//============================================================================== +void LatestVersionCheckerAndUpdater::run() +{ + queryUpdateServer(); - return major > b.major; + if (! threadShouldExit()) + MessageManager::callAsync ([this] { processResult(); }); } //============================================================================== -struct RelaunchTimer : private Timer +String getOSString() +{ + #if JUCE_MAC + return "OSX"; + #elif JUCE_WINDOWS + return "Windows"; + #elif JUCE_LINUX + return "Linux"; + #else + jassertfalse; + return "Unknown"; + #endif +} + +namespace VersionHelpers { - RelaunchTimer (const File& f) : parentFolder (f) + String formatProductVersion (int versionNum) { - startTimer (1500); + int major = (versionNum & 0xff0000) >> 16; + int minor = (versionNum & 0x00ff00) >> 8; + int build = (versionNum & 0x0000ff) >> 0; + + return String (major) + '.' + String (minor) + '.' + String (build); } - void timerCallback() override + String getProductVersionString() { - stopTimer(); + return formatProductVersion (ProjectInfo::versionNumber); + } - File app = parentFolder.getChildFile ( - #if JUCE_MAC - "Projucer.app"); - #elif JUCE_WINDOWS - "Projucer.exe"); - #elif JUCE_LINUX - "Projucer"); - #endif + bool isNewVersion (const String& current, const String& other) + { + auto currentTokens = StringArray::fromTokens (current, ".", {}); + auto otherTokens = StringArray::fromTokens (other, ".", {}); - JUCEApplication::quit(); + jassert (currentTokens.size() == 3 && otherTokens.size() == 3); - if (app.exists()) + if (currentTokens[0].getIntValue() == otherTokens[0].getIntValue()) { - app.setExecutePermission (true); + if (currentTokens[1].getIntValue() == otherTokens[1].getIntValue()) + return currentTokens[2].getIntValue() < otherTokens[2].getIntValue(); - #if JUCE_MAC - app.getChildFile ("Contents") - .getChildFile ("MacOS") - .getChildFile ("Projucer").setExecutePermission (true); - #endif - - app.startAsProcess(); + return currentTokens[1].getIntValue() < otherTokens[1].getIntValue(); } - delete this; + return currentTokens[0].getIntValue() < otherTokens[0].getIntValue(); } +} - File parentFolder; -}; - -//============================================================================== -class DownloadNewVersionThread : public ThreadWithProgressWindow +void LatestVersionCheckerAndUpdater::queryUpdateServer() { -public: - DownloadNewVersionThread (LatestVersionChecker& versionChecker,URL u, - const String& extraHeaders, File target) - : ThreadWithProgressWindow ("Downloading New Version", true, true), - owner (versionChecker), - result (Result::ok()), - url (u), headers (extraHeaders), targetFolder (target) - { - } + StringPairArray responseHeaders; - static void performDownload (LatestVersionChecker& versionChecker, URL u, - const String& extraHeaders, File targetFolder) - { - DownloadNewVersionThread d (versionChecker, u, extraHeaders, targetFolder); + URL latestVersionURL ("https://my.roli.com/software_versions/update_to/Projucer/" + + VersionHelpers::getProductVersionString() + '/' + getOSString() + + "?language=" + SystemStats::getUserLanguage()); - if (d.runThread()) - { - if (d.result.failed()) - { - AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, - "Installation Failed", - d.result.getErrorMessage()); - } - else - { - new RelaunchTimer (targetFolder); - } - } - } + std::unique_ptr inStream (latestVersionURL.createInputStream (false, nullptr, nullptr, + "X-API-Key: 265441b-343403c-20f6932-76361d\nContent-Type: " + "application/json\nAccept: application/json; version=1", + 0, &responseHeaders, &statusCode, 0)); - void run() override + if (threadShouldExit()) + return; + + if (inStream.get() != nullptr && (statusCode == 303 || statusCode == 400)) { - setProgress (-1.0); + if (statusCode == 303) + relativeDownloadPath = responseHeaders["Location"]; - MemoryBlock zipData; - result = download (zipData); + jassert (relativeDownloadPath.isNotEmpty()); - if (result.wasOk() && ! threadShouldExit()) - { - setStatusMessage ("Installing..."); - result = owner.performUpdate (zipData, targetFolder); - } + jsonReply = JSON::parse (inStream->readEntireStreamAsString()); } - - Result download (MemoryBlock& dest) + else if (showAlertWindows) { - setStatusMessage ("Downloading..."); - - int statusCode = 302; - const int maxRedirects = 5; - - // we need to do the redirecting manually due to inconsistencies on the way headers are handled on redirects - std::unique_ptr in; - - for (int redirect = 0; redirect < maxRedirects; ++redirect) - { - StringPairArray responseHeaders; - - in.reset (url.createInputStream (false, nullptr, nullptr, headers, - 10000, &responseHeaders, &statusCode, 0)); - - if (in == nullptr || statusCode != 302) - break; + if (statusCode == 204) + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, "No New Version Available", "Your JUCE version is up to date."); + else + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Network Error", "Could not connect to the web server.\n" + "Please check your internet connection and try again."); + } +} - String redirectPath = responseHeaders ["Location"]; - if (redirectPath.isEmpty()) - break; +void LatestVersionCheckerAndUpdater::processResult() +{ + if (! jsonReply.isObject()) + return; - url = owner.getLatestVersionURL (headers, redirectPath); - } + if (statusCode == 400) + { + auto errorObject = jsonReply.getDynamicObject()->getProperty ("error"); - if (in != nullptr && statusCode == 200) + if (errorObject.isObject()) { - int64 total = 0; - MemoryOutputStream mo (dest, true); - - for (;;) - { - if (threadShouldExit()) - return Result::fail ("cancel"); - - int64 written = mo.writeFromInputStream (*in, 8192); - - if (written == 0) - break; - - total += written; - - setStatusMessage (String (TRANS("Downloading... (123)")) - .replace ("123", File::descriptionOfSizeInBytes (total))); - } + auto message = errorObject.getProperty ("message", {}).toString(); - return Result::ok(); + if (message.isNotEmpty()) + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "JUCE Updater", message); } - - return Result::fail ("Failed to download from: " + url.toString (false)); } - - LatestVersionChecker& owner; - Result result; - URL url; - String headers; - File targetFolder; - - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DownloadNewVersionThread) -}; + else if (statusCode == 303) + { + askUserAboutNewVersion (jsonReply.getProperty ("version", {}).toString(), + jsonReply.getProperty ("notes", {}).toString()); + } +} //============================================================================== -class UpdateUserDialog : public Component +class UpdateDialog : public Component { public: - UpdateUserDialog (const LatestVersionChecker::JuceVersionTriple& version, - const String& productName, - const String& releaseNotes, - const char* overwriteFolderPath) - : hasOverwriteButton (overwriteFolderPath != nullptr) + UpdateDialog (const String& newVersion, const String& releaseNotes) { - titleLabel.reset (new Label ("Title Label", - TRANS("Download \"123\" version 456?") - .replace ("123", productName) - .replace ("456", version.toString()))); - addAndMakeVisible (titleLabel.get()); - - titleLabel->setFont (Font (15.00f, Font::bold)); - titleLabel->setJustificationType (Justification::centredLeft); - titleLabel->setEditable (false, false, false); - - contentLabel.reset(new Label ("Content Label", - TRANS("A new version of \"123\" is available - would you like to download it?") - .replace ("123", productName))); - addAndMakeVisible (contentLabel.get()); - contentLabel->setFont (Font (15.00f, Font::plain)); - contentLabel->setJustificationType (Justification::topLeft); - contentLabel->setEditable (false, false, false); - - okButton.reset (new TextButton ("OK Button")); - addAndMakeVisible (okButton.get()); - okButton->setButtonText (TRANS(hasOverwriteButton ? "Choose Another Folder..." : "OK")); - okButton->onClick = [this] { exitParentDialog (2); }; - - cancelButton.reset (new TextButton ("Cancel Button")); - addAndMakeVisible (cancelButton.get()); - cancelButton->setButtonText (TRANS("Cancel")); - cancelButton->onClick = [this] { exitParentDialog (-1); }; - - changeLogLabel.reset (new Label ("Change Log Label", TRANS("Release Notes:"))); - addAndMakeVisible (changeLogLabel.get()); - changeLogLabel->setFont (Font (15.00f, Font::plain)); - changeLogLabel->setJustificationType (Justification::topLeft); - changeLogLabel->setEditable (false, false, false); - - changeLog.reset (new TextEditor ("Change Log")); - addAndMakeVisible (changeLog.get()); - changeLog->setMultiLine (true); - changeLog->setReturnKeyStartsNewLine (true); - changeLog->setReadOnly (true); - changeLog->setScrollbarsShown (true); - changeLog->setCaretVisible (false); - changeLog->setPopupMenuEnabled (false); - changeLog->setText (releaseNotes); - - if (hasOverwriteButton) + titleLabel.setText ("JUCE version " + newVersion, dontSendNotification); + titleLabel.setFont ({ 15.0f, Font::bold }); + titleLabel.setJustificationType (Justification::centred); + addAndMakeVisible (titleLabel); + + contentLabel.setText ("A new version of JUCE is available - would you like to download it?", dontSendNotification); + contentLabel.setFont (15.0f); + contentLabel.setJustificationType (Justification::topLeft); + addAndMakeVisible (contentLabel); + + releaseNotesLabel.setText ("Release notes:", dontSendNotification); + releaseNotesLabel.setFont (15.0f); + releaseNotesLabel.setJustificationType (Justification::topLeft); + addAndMakeVisible (releaseNotesLabel); + + releaseNotesEditor.setMultiLine (true); + releaseNotesEditor.setReadOnly (true); + releaseNotesEditor.setText (releaseNotes); + addAndMakeVisible (releaseNotesEditor); + + addAndMakeVisible (chooseButton); + chooseButton.onClick = [this] { exitModalStateWithResult (1); }; + + addAndMakeVisible (cancelButton); + cancelButton.onClick = [this] { - overwriteLabel.reset (new Label ("Overwrite Label", - TRANS("Updating will overwrite everything in the following folder:"))); - addAndMakeVisible (overwriteLabel.get()); - overwriteLabel->setFont (Font (15.00f, Font::plain)); - overwriteLabel->setJustificationType (Justification::topLeft); - overwriteLabel->setEditable (false, false, false); - - overwritePath.reset (new Label ("Overwrite Path", overwriteFolderPath)); - addAndMakeVisible (overwritePath.get()); - overwritePath->setFont (Font (15.00f, Font::bold)); - overwritePath->setJustificationType (Justification::topLeft); - overwritePath->setEditable (false, false, false); - - overwriteButton.reset (new TextButton ("Overwrite Button")); - addAndMakeVisible (overwriteButton.get()); - overwriteButton->setButtonText (TRANS("Overwrite")); - overwriteButton->onClick = [this] { exitParentDialog (1); }; - } + if (dontAskAgainButton.getToggleState()) + getGlobalProperties().setValue (Ids::dontQueryForUpdate.toString(), 1); + else + getGlobalProperties().removeValue (Ids::dontQueryForUpdate); - juceIcon.reset (Drawable::createFromImageData (BinaryData::juce_icon_png, - BinaryData::juce_icon_pngSize)); + exitModalStateWithResult (-1); + }; - setSize (518, overwritePath != nullptr ? 345 : 269); + dontAskAgainButton.setToggleState (getGlobalProperties().getValue (Ids::dontQueryForUpdate, {}).isNotEmpty(), dontSendNotification); + addAndMakeVisible (dontAskAgainButton); + + juceIcon.reset (Drawable::createFromImageData (BinaryData::juce_icon_png, BinaryData::juce_icon_pngSize)); lookAndFeelChanged(); + + setSize (500, 280); } - ~UpdateUserDialog() override + void resized() override { - titleLabel.reset(); - contentLabel.reset(); - okButton.reset(); - cancelButton.reset(); - changeLogLabel.reset(); - changeLog.reset(); - overwriteLabel.reset(); - overwritePath.reset(); - overwriteButton.reset(); - juceIcon.reset(); + auto b = getLocalBounds().reduced (10); + + auto topSlice = b.removeFromTop (juceIconBounds.getHeight()) + .withTrimmedLeft (juceIconBounds.getWidth()); + + titleLabel.setBounds (topSlice.removeFromTop (25)); + topSlice.removeFromTop (5); + contentLabel.setBounds (topSlice.removeFromTop (25)); + + auto buttonBounds = b.removeFromBottom (60); + buttonBounds.removeFromBottom (25); + chooseButton.setBounds (buttonBounds.removeFromLeft (buttonBounds.getWidth() / 2).reduced (20, 0)); + cancelButton.setBounds (buttonBounds.reduced (20, 0)); + dontAskAgainButton.setBounds (cancelButton.getBounds().withY (cancelButton.getBottom() + 5).withHeight (20)); + + releaseNotesEditor.setBounds (b.reduced (0, 10)); } void paint (Graphics& g) override { g.fillAll (findColour (backgroundColourId)); - g.setColour (findColour (defaultTextColourId)); if (juceIcon != nullptr) - juceIcon->drawWithin (g, Rectangle (20, 17, 64, 64), - RectanglePlacement::stretchToFit, 1.000f); + juceIcon->drawWithin (g, juceIconBounds.toFloat(), + RectanglePlacement::stretchToFit, 1.0f); } - void resized() override + static std::unique_ptr launchDialog (const String& newVersion, const String& releaseNotes) { - titleLabel->setBounds (88, 10, 397, 24); - contentLabel->setBounds (88, 40, 397, 51); - changeLogLabel->setBounds (22, 92, 341, 24); - changeLog->setBounds (24, 112, 476, 102); + DialogWindow::LaunchOptions options; - if (hasOverwriteButton) - { - okButton->setBounds (getWidth() - 24 - 174, getHeight() - 37, 174, 28); - overwriteButton->setBounds ((getWidth() - 24 - 174) + -14 - 86, getHeight() - 37, 86, 28); - cancelButton->setBounds (24, getHeight() - 37, 70, 28); + options.dialogTitle = "Download JUCE version " + newVersion + "?"; + options.resizable = false; - overwriteLabel->setBounds (24, 238, 472, 16); - overwritePath->setBounds (24, 262, 472, 40); - } - else - { - okButton->setBounds (getWidth() - 24 - 47, getHeight() - 37, 47, 28); - cancelButton->setBounds ((getWidth() - 24 - 47) + -14 - 70, getHeight() - 37, 70, 28); - } + auto* content = new UpdateDialog (newVersion, releaseNotes); + options.content.set (content, true); + + std::unique_ptr dialog (options.create()); + + content->setParentWindow (dialog.get()); + dialog->enterModalState (true, nullptr, true); + + return dialog; } - void exitParentDialog (int returnVal) +private: + void lookAndFeelChanged() override { - if (auto* parentDialog = findParentComponentOfClass()) - parentDialog->exitModalState (returnVal); - else - jassertfalse; + cancelButton.setColour (TextButton::buttonColourId, findColour (secondaryButtonBackgroundColourId)); + releaseNotesEditor.applyFontToAllText (releaseNotesEditor.getFont()); } - static DialogWindow* launch (const LatestVersionChecker::JuceVersionTriple& version, - const String& productName, - const String& releaseNotes, - const char* overwritePath = nullptr) + void setParentWindow (DialogWindow* parent) { - OptionalScopedPointer userDialog (new UpdateUserDialog (version, productName, - releaseNotes, overwritePath), true); - - DialogWindow::LaunchOptions lo; - lo.dialogTitle = TRANS("Download \"123\" version 456?").replace ("456", version.toString()) - .replace ("123", productName); - lo.dialogBackgroundColour = userDialog->findColour (backgroundColourId); - lo.content = userDialog; - lo.componentToCentreAround = nullptr; - lo.escapeKeyTriggersCloseButton = true; - lo.useNativeTitleBar = true; - lo.resizable = false; - lo.useBottomRightCornerResizer = false; - - return lo.launchAsync(); + parentWindow = parent; } -private: - bool hasOverwriteButton; - std::unique_ptr