/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2013 - Raw Material Software Ltd. Permission is granted to use this software under the terms of either: a) the GPL v2 (or any later version) b) the Affero GPL v3 Details of these licenses can be found at: www.gnu.org/licenses JUCE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. ------------------------------------------------------------------------------ To release a closed-source product which uses JUCE, commercial licenses are available: visit www.juce.com for more information. ============================================================================== */ #ifndef JUCER_AUTOUPDATER_H_INCLUDED #define JUCER_AUTOUPDATER_H_INCLUDED //============================================================================== class LatestVersionChecker : private Thread, private Timer { public: LatestVersionChecker() : Thread ("Updater"), hasAttemptedToReadWebsite (false) { startTimer (2000); } ~LatestVersionChecker() { stopThread (20000); } static URL getLatestVersionURL() { return URL ("http://www.juce.com/juce/updates/updatelist.php"); } void checkForNewVersion() { hasAttemptedToReadWebsite = true; { const ScopedPointer in (getLatestVersionURL().createInputStream (false)); if (in == nullptr || threadShouldExit()) return; // can't connect: fail silently. jsonReply = JSON::parse (in->readEntireStreamAsString()); } if (threadShouldExit()) return; if (jsonReply.isArray() || jsonReply.isObject()) startTimer (100); } void processResult (var reply) { if (reply.isArray()) { askUserAboutNewVersion (VersionInfo (reply[0])); } else if (reply.isObject()) { // In the far-distant future, this may be contacting a defunct // URL, so hopefully the website will contain a helpful message // for the user.. String message = reply.getProperty ("message", var()).toString(); if (message.isNotEmpty()) { AlertWindow::showMessageBox (AlertWindow::WarningIcon, TRANS("JUCE Updater"), message); } } } struct VersionInfo { VersionInfo (var v) { version = v.getProperty ("version", var()).toString().trim(); url = v.getProperty ( #if JUCE_MAC "url_osx", #elif JUCE_WINDOWS "url_win", #elif JUCE_LINUX "url_linux", #endif var()).toString(); } bool isDifferentVersionToCurrent() const { return version != JUCE_STRINGIFY(JUCE_MAJOR_VERSION) "." JUCE_STRINGIFY(JUCE_MINOR_VERSION) "." JUCE_STRINGIFY(JUCE_BUILDNUMBER) && version.containsChar ('.') && version.length() > 2; } String version; URL url; }; void askUserAboutNewVersion (const VersionInfo& info) { if (info.isDifferentVersionToCurrent()) { File appParentFolder (File::getSpecialLocation (File::currentApplicationFile).getParentDirectory()); if (isZipFolder (appParentFolder)) { int result = AlertWindow::showYesNoCancelBox (AlertWindow::InfoIcon, TRANS("Download JUCE version 123?").replace ("123", info.version), TRANS("A new version of JUCE is available - would you like to overwrite the folder:\n\n" "xfldrx\n\n" " ..with the latest version from juce.com?\n\n" "(Please note that this will overwrite everything in that folder!)") .replace ("xfldrx", appParentFolder.getFullPathName()), TRANS("Overwrite"), TRANS("Choose another folder..."), TRANS("Cancel")); if (result == 1) DownloadNewVersionThread::performDownload (info.url, appParentFolder); if (result == 2) askUserForLocationToDownload (info); } else { if (AlertWindow::showOkCancelBox (AlertWindow::InfoIcon, TRANS("Download JUCE version 123?").replace ("123", info.version), TRANS("A new version of JUCE is available - would you like to download it?"))) askUserForLocationToDownload (info); } } } void askUserForLocationToDownload (const VersionInfo& info) { File targetFolder (findDefaultModulesFolder()); if (isJuceModulesFolder (targetFolder)) targetFolder = targetFolder.getParentDirectory(); FileChooser chooser (TRANS("Please select the location into which you'd like to install the new version"), targetFolder); if (chooser.browseForDirectory()) { targetFolder = chooser.getResult(); if (isJuceModulesFolder (targetFolder)) targetFolder = targetFolder.getParentDirectory(); if (targetFolder.getChildFile ("JUCE").isDirectory()) targetFolder = targetFolder.getChildFile ("JUCE"); if (targetFolder.getChildFile (".git").isDirectory()) { AlertWindow::showMessageBox (AlertWindow::WarningIcon, TRANS ("Downloading new JUCE version"), TRANS ("This folder is a GIT repository!\n\n" "You should use a \"git pull\" to update it to the latest version. " "Or to use the Introjucer to get an update, you should select an empty " "folder into which you'd like to download the new code.")); return; } if (isJuceFolder (targetFolder)) { if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, TRANS("Overwrite existing JUCE folder?"), TRANS("Do you want to overwrite the folder:\n\n" "xfldrx\n\n" " ..with the latest version from juce.com?\n\n" "(Please note that this will overwrite everything in that folder!)") .replace ("xfldrx", targetFolder.getFullPathName()))) { return; } } else { targetFolder = targetFolder.getChildFile ("JUCE").getNonexistentSibling(); } DownloadNewVersionThread::performDownload (info.url, targetFolder); } } static bool isZipFolder (const File& f) { return f.getChildFile ("modules").isDirectory() && f.getChildFile ("extras").isDirectory() && f.getChildFile ("examples").isDirectory() && ! f.getChildFile (".git").isDirectory(); } private: void timerCallback() override { stopTimer(); if (hasAttemptedToReadWebsite) processResult (jsonReply); else startThread (3); } void run() override { checkForNewVersion(); } var jsonReply; bool hasAttemptedToReadWebsite; URL newVersionToDownload; //============================================================================== class DownloadNewVersionThread : public ThreadWithProgressWindow { public: DownloadNewVersionThread (URL u, File target) : ThreadWithProgressWindow ("Downloading New Version", true, true), result (Result::ok()), url (u), targetFolder (target) { } static void performDownload (URL u, File targetFolder) { DownloadNewVersionThread d (u, targetFolder); if (d.runThread()) { if (d.result.failed()) { AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Installation Failed", d.result.getErrorMessage()); } else { new RelaunchTimer (targetFolder); } } } void run() override { setProgress (-1.0); MemoryBlock zipData; result = download (zipData); if (result.wasOk() && ! threadShouldExit()) result = unzip (zipData); } Result download (MemoryBlock& dest) { setStatusMessage ("Downloading..."); const ScopedPointer in (url.createInputStream (false, nullptr, nullptr, String::empty, 10000)); if (in != nullptr) { 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))); } return Result::ok(); } return Result::fail ("Failed to download from: " + url.toString (false)); } Result unzip (const MemoryBlock& data) { setStatusMessage ("Installing..."); File unzipTarget; bool isUsingTempFolder = false; { MemoryInputStream input (data, false); ZipFile zip (input); if (zip.getNumEntries() == 0) return Result::fail ("The downloaded file wasn't a valid JUCE file!"); unzipTarget = targetFolder; if (unzipTarget.exists()) { isUsingTempFolder = true; unzipTarget = targetFolder.getNonexistentSibling(); if (! unzipTarget.createDirectory()) return Result::fail ("Couldn't create a folder to unzip the new version!"); } Result r (zip.uncompressTo (unzipTarget)); if (r.failed()) { if (isUsingTempFolder) unzipTarget.deleteRecursively(); return r; } } if (isUsingTempFolder) { File oldFolder (targetFolder.getSiblingFile (targetFolder.getFileNameWithoutExtension() + "_old").getNonexistentSibling()); if (! targetFolder.moveFileTo (oldFolder)) { unzipTarget.deleteRecursively(); return Result::fail ("Could not remove the existing folder!"); } if (! unzipTarget.moveFileTo (targetFolder)) { unzipTarget.deleteRecursively(); return Result::fail ("Could not overwrite the existing folder!"); } } return Result::ok(); } Result result; URL url; File targetFolder; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DownloadNewVersionThread) }; struct RelaunchTimer : private Timer { RelaunchTimer (const File& f) : parentFolder (f) { startTimer (1500); } void timerCallback() override { stopTimer(); File app = parentFolder.getChildFile ( #if JUCE_MAC "Introjucer.app"); #elif JUCE_WINDOWS "Introjucer.exe"); #elif JUCE_LINUX "Introjucer"); #endif JUCEApplication::quit(); if (app.exists()) { app.setExecutePermission (true); #if JUCE_MAC app.getChildFile("Contents") .getChildFile("MacOS") .getChildFile("Introjucer").setExecutePermission (true); #endif app.startAsProcess(); } delete this; } File parentFolder; }; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LatestVersionChecker) }; #endif // JUCER_AUTOUPDATER_H_INCLUDED