The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

505 lines
19KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. #include "../Application/jucer_Headers.h"
  20. #include "jucer_Application.h"
  21. #include "jucer_AutoUpdater.h"
  22. //==============================================================================
  23. LatestVersionCheckerAndUpdater::LatestVersionCheckerAndUpdater()
  24. : Thread ("VersionChecker")
  25. {
  26. }
  27. LatestVersionCheckerAndUpdater::~LatestVersionCheckerAndUpdater()
  28. {
  29. stopThread (1000);
  30. clearSingletonInstance();
  31. }
  32. void LatestVersionCheckerAndUpdater::checkForNewVersion (bool showAlerts)
  33. {
  34. if (! isThreadRunning())
  35. {
  36. showAlertWindows = showAlerts;
  37. startThread (3);
  38. }
  39. }
  40. //==============================================================================
  41. void LatestVersionCheckerAndUpdater::run()
  42. {
  43. auto info = VersionInfo::fetchLatestFromUpdateServer();
  44. if (info == nullptr)
  45. {
  46. if (showAlertWindows)
  47. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  48. "Update Server Communication Error",
  49. "Failed to communicate with the JUCE update server.\n"
  50. "Please try again in a few minutes.\n\n"
  51. "If this problem persists you can download the latest version of JUCE from juce.com");
  52. return;
  53. }
  54. if (! info->isNewerVersionThanCurrent())
  55. {
  56. if (showAlertWindows)
  57. AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon,
  58. "No New Version Available",
  59. "Your JUCE version is up to date.");
  60. return;
  61. }
  62. auto osString = []
  63. {
  64. #if JUCE_MAC
  65. return "osx";
  66. #elif JUCE_WINDOWS
  67. return "windows";
  68. #elif JUCE_LINUX
  69. return "linux";
  70. #else
  71. jassertfalse;
  72. return "Unknown";
  73. #endif
  74. }();
  75. String requiredFilename ("juce-" + info->versionString + "-" + osString + ".zip");
  76. for (auto& asset : info->assets)
  77. {
  78. if (asset.name == requiredFilename)
  79. {
  80. auto versionString = info->versionString;
  81. auto releaseNotes = info->releaseNotes;
  82. MessageManager::callAsync ([this, versionString, releaseNotes, asset]
  83. {
  84. askUserAboutNewVersion (versionString, releaseNotes, asset);
  85. });
  86. return;
  87. }
  88. }
  89. if (showAlertWindows)
  90. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  91. "Failed to find any new downloads",
  92. "Please try again in a few minutes.");
  93. }
  94. //==============================================================================
  95. class UpdateDialog : public Component
  96. {
  97. public:
  98. UpdateDialog (const String& newVersion, const String& releaseNotes)
  99. {
  100. titleLabel.setText ("JUCE version " + newVersion, dontSendNotification);
  101. titleLabel.setFont ({ 15.0f, Font::bold });
  102. titleLabel.setJustificationType (Justification::centred);
  103. addAndMakeVisible (titleLabel);
  104. contentLabel.setText ("A new version of JUCE is available - would you like to download it?", dontSendNotification);
  105. contentLabel.setFont (15.0f);
  106. contentLabel.setJustificationType (Justification::topLeft);
  107. addAndMakeVisible (contentLabel);
  108. releaseNotesLabel.setText ("Release notes:", dontSendNotification);
  109. releaseNotesLabel.setFont (15.0f);
  110. releaseNotesLabel.setJustificationType (Justification::topLeft);
  111. addAndMakeVisible (releaseNotesLabel);
  112. releaseNotesEditor.setMultiLine (true);
  113. releaseNotesEditor.setReadOnly (true);
  114. releaseNotesEditor.setText (releaseNotes);
  115. addAndMakeVisible (releaseNotesEditor);
  116. addAndMakeVisible (chooseButton);
  117. chooseButton.onClick = [this] { exitModalStateWithResult (1); };
  118. addAndMakeVisible (cancelButton);
  119. cancelButton.onClick = [this]
  120. {
  121. if (dontAskAgainButton.getToggleState())
  122. getGlobalProperties().setValue (Ids::dontQueryForUpdate.toString(), 1);
  123. else
  124. getGlobalProperties().removeValue (Ids::dontQueryForUpdate);
  125. exitModalStateWithResult (-1);
  126. };
  127. dontAskAgainButton.setToggleState (getGlobalProperties().getValue (Ids::dontQueryForUpdate, {}).isNotEmpty(), dontSendNotification);
  128. addAndMakeVisible (dontAskAgainButton);
  129. juceIcon = Drawable::createFromImageData (BinaryData::juce_icon_png,
  130. BinaryData::juce_icon_pngSize);
  131. lookAndFeelChanged();
  132. setSize (500, 280);
  133. }
  134. void resized() override
  135. {
  136. auto b = getLocalBounds().reduced (10);
  137. auto topSlice = b.removeFromTop (juceIconBounds.getHeight())
  138. .withTrimmedLeft (juceIconBounds.getWidth());
  139. titleLabel.setBounds (topSlice.removeFromTop (25));
  140. topSlice.removeFromTop (5);
  141. contentLabel.setBounds (topSlice.removeFromTop (25));
  142. auto buttonBounds = b.removeFromBottom (60);
  143. buttonBounds.removeFromBottom (25);
  144. chooseButton.setBounds (buttonBounds.removeFromLeft (buttonBounds.getWidth() / 2).reduced (20, 0));
  145. cancelButton.setBounds (buttonBounds.reduced (20, 0));
  146. dontAskAgainButton.setBounds (cancelButton.getBounds().withY (cancelButton.getBottom() + 5).withHeight (20));
  147. releaseNotesEditor.setBounds (b.reduced (0, 10));
  148. }
  149. void paint (Graphics& g) override
  150. {
  151. g.fillAll (findColour (backgroundColourId));
  152. if (juceIcon != nullptr)
  153. juceIcon->drawWithin (g, juceIconBounds.toFloat(),
  154. RectanglePlacement::stretchToFit, 1.0f);
  155. }
  156. static std::unique_ptr<DialogWindow> launchDialog (const String& newVersionString,
  157. const String& releaseNotes)
  158. {
  159. DialogWindow::LaunchOptions options;
  160. options.dialogTitle = "Download JUCE version " + newVersionString + "?";
  161. options.resizable = false;
  162. auto* content = new UpdateDialog (newVersionString, releaseNotes);
  163. options.content.set (content, true);
  164. std::unique_ptr<DialogWindow> dialog (options.create());
  165. content->setParentWindow (dialog.get());
  166. dialog->enterModalState (true, nullptr, true);
  167. return dialog;
  168. }
  169. private:
  170. void lookAndFeelChanged() override
  171. {
  172. cancelButton.setColour (TextButton::buttonColourId, findColour (secondaryButtonBackgroundColourId));
  173. releaseNotesEditor.applyFontToAllText (releaseNotesEditor.getFont());
  174. }
  175. void setParentWindow (DialogWindow* parent)
  176. {
  177. parentWindow = parent;
  178. }
  179. void exitModalStateWithResult (int result)
  180. {
  181. if (parentWindow != nullptr)
  182. parentWindow->exitModalState (result);
  183. }
  184. Label titleLabel, contentLabel, releaseNotesLabel;
  185. TextEditor releaseNotesEditor;
  186. TextButton chooseButton { "Choose Location..." }, cancelButton { "Cancel" };
  187. ToggleButton dontAskAgainButton { "Don't ask again" };
  188. std::unique_ptr<Drawable> juceIcon;
  189. Rectangle<int> juceIconBounds { 10, 10, 64, 64 };
  190. DialogWindow* parentWindow = nullptr;
  191. };
  192. void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const VersionInfo::Asset& asset)
  193. {
  194. FileChooser chooser ("Please select the location into which you would like to install the new version",
  195. { getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get() });
  196. if (chooser.browseForDirectory())
  197. {
  198. auto targetFolder = chooser.getResult();
  199. // By default we will install into 'targetFolder/JUCE', but we should install into
  200. // 'targetFolder' if that is an existing JUCE directory.
  201. bool willOverwriteJuceFolder = [&targetFolder]
  202. {
  203. if (isJUCEFolder (targetFolder))
  204. return true;
  205. targetFolder = targetFolder.getChildFile ("JUCE");
  206. return isJUCEFolder (targetFolder);
  207. }();
  208. auto targetFolderPath = targetFolder.getFullPathName();
  209. if (willOverwriteJuceFolder)
  210. {
  211. if (targetFolder.getChildFile (".git").isDirectory())
  212. {
  213. AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, "Downloading New JUCE Version",
  214. targetFolderPath + "\n\nis a GIT repository!\n\nYou should use a \"git pull\" to update it to the latest version.");
  215. return;
  216. }
  217. if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Overwrite Existing JUCE Folder?",
  218. "Do you want to replace the folder\n\n" + targetFolderPath + "\n\nwith the latest version from juce.com?\n\n"
  219. "This will move the existing folder to " + targetFolderPath + "_old."))
  220. {
  221. return;
  222. }
  223. }
  224. else if (targetFolder.exists())
  225. {
  226. if (! AlertWindow::showOkCancelBox (AlertWindow::WarningIcon, "Existing File Or Directory",
  227. "Do you want to move\n\n" + targetFolderPath + "\n\nto\n\n" + targetFolderPath + "_old?"))
  228. {
  229. return;
  230. }
  231. }
  232. downloadAndInstall (asset, targetFolder);
  233. }
  234. }
  235. void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersionString,
  236. const String& releaseNotes,
  237. const VersionInfo::Asset& asset)
  238. {
  239. dialogWindow = UpdateDialog::launchDialog (newVersionString, releaseNotes);
  240. if (auto* mm = ModalComponentManager::getInstance())
  241. mm->attachCallback (dialogWindow.get(),
  242. ModalCallbackFunction::create ([this, asset] (int result)
  243. {
  244. if (result == 1)
  245. askUserForLocationToDownload (asset);
  246. dialogWindow.reset();
  247. }));
  248. }
  249. //==============================================================================
  250. class DownloadAndInstallThread : private ThreadWithProgressWindow
  251. {
  252. public:
  253. DownloadAndInstallThread (const VersionInfo::Asset& a, const File& t, std::function<void()>&& cb)
  254. : ThreadWithProgressWindow ("Downloading New Version", true, true),
  255. asset (a), targetFolder (t), completionCallback (std::move (cb))
  256. {
  257. launchThread (3);
  258. }
  259. private:
  260. void run() override
  261. {
  262. setProgress (-1.0);
  263. MemoryBlock zipData;
  264. auto result = download (zipData);
  265. if (result.wasOk() && ! threadShouldExit())
  266. result = install (zipData);
  267. if (result.failed())
  268. MessageManager::callAsync ([result] { AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
  269. "Installation Failed",
  270. result.getErrorMessage()); });
  271. else
  272. MessageManager::callAsync (completionCallback);
  273. }
  274. Result download (MemoryBlock& dest)
  275. {
  276. setStatusMessage ("Downloading...");
  277. int statusCode = 0;
  278. auto inStream = VersionInfo::createInputStreamForAsset (asset, statusCode);
  279. if (inStream != nullptr && statusCode == 200)
  280. {
  281. int64 total = 0;
  282. MemoryOutputStream mo (dest, true);
  283. for (;;)
  284. {
  285. if (threadShouldExit())
  286. return Result::fail ("Cancelled");
  287. auto written = mo.writeFromInputStream (*inStream, 8192);
  288. if (written == 0)
  289. break;
  290. total += written;
  291. setStatusMessage ("Downloading... " + File::descriptionOfSizeInBytes (total));
  292. }
  293. return Result::ok();
  294. }
  295. return Result::fail ("Failed to download from: " + asset.url);
  296. }
  297. Result install (const MemoryBlock& data)
  298. {
  299. setStatusMessage ("Installing...");
  300. MemoryInputStream input (data, false);
  301. ZipFile zip (input);
  302. if (zip.getNumEntries() == 0)
  303. return Result::fail ("The downloaded file was not a valid JUCE file!");
  304. struct ScopedDownloadFolder
  305. {
  306. ScopedDownloadFolder (const File& installTargetFolder)
  307. {
  308. folder = installTargetFolder.getSiblingFile (installTargetFolder.getFileNameWithoutExtension() + "_download").getNonexistentSibling();
  309. jassert (folder.createDirectory());
  310. }
  311. ~ScopedDownloadFolder() { folder.deleteRecursively(); }
  312. File folder;
  313. };
  314. ScopedDownloadFolder unzipTarget (targetFolder);
  315. if (! unzipTarget.folder.isDirectory())
  316. return Result::fail ("Couldn't create a temporary folder to unzip the new version!");
  317. auto r = zip.uncompressTo (unzipTarget.folder);
  318. if (r.failed())
  319. return r;
  320. if (threadShouldExit())
  321. return Result::fail ("Cancelled");
  322. #if JUCE_LINUX || JUCE_MAC
  323. r = setFilePermissions (unzipTarget.folder, zip);
  324. if (r.failed())
  325. return r;
  326. if (threadShouldExit())
  327. return Result::fail ("Cancelled");
  328. #endif
  329. if (targetFolder.exists())
  330. {
  331. auto oldFolder = targetFolder.getSiblingFile (targetFolder.getFileNameWithoutExtension() + "_old").getNonexistentSibling();
  332. if (! targetFolder.moveFileTo (oldFolder))
  333. return Result::fail ("Could not remove the existing folder!\n\n"
  334. "This may happen if you are trying to download into a directory that requires administrator privileges to modify.\n"
  335. "Please select a folder that is writable by the current user.");
  336. }
  337. if (! unzipTarget.folder.getChildFile ("JUCE").moveFileTo (targetFolder))
  338. return Result::fail ("Could not overwrite the existing folder!\n\n"
  339. "This may happen if you are trying to download into a directory that requires administrator privileges to modify.\n"
  340. "Please select a folder that is writable by the current user.");
  341. return Result::ok();
  342. }
  343. Result setFilePermissions (const File& root, const ZipFile& zip)
  344. {
  345. constexpr uint32 executableFlag = (1 << 22);
  346. for (int i = 0; i < zip.getNumEntries(); ++i)
  347. {
  348. auto* entry = zip.getEntry (i);
  349. if ((entry->externalFileAttributes & executableFlag) != 0 && entry->filename.getLastCharacter() != '/')
  350. {
  351. auto exeFile = root.getChildFile (entry->filename);
  352. if (! exeFile.exists())
  353. return Result::fail ("Failed to find executable file when setting permissions " + exeFile.getFileName());
  354. if (! exeFile.setExecutePermission (true))
  355. return Result::fail ("Failed to set executable file permission for " + exeFile.getFileName());
  356. }
  357. }
  358. return Result::ok();
  359. }
  360. VersionInfo::Asset asset;
  361. File targetFolder;
  362. std::function<void()> completionCallback;
  363. };
  364. void restartProcess (const File& targetFolder)
  365. {
  366. #if JUCE_MAC || JUCE_LINUX
  367. #if JUCE_MAC
  368. auto newProcess = targetFolder.getChildFile ("Projucer.app").getChildFile ("Contents").getChildFile ("MacOS").getChildFile ("Projucer");
  369. #elif JUCE_LINUX
  370. auto newProcess = targetFolder.getChildFile ("Projucer");
  371. #endif
  372. StringArray command ("/bin/sh", "-c", "while killall -0 Projucer; do sleep 5; done; " + newProcess.getFullPathName().quoted());
  373. #elif JUCE_WINDOWS
  374. auto newProcess = targetFolder.getChildFile ("Projucer.exe");
  375. auto command = "cmd.exe /c\"@echo off & for /l %a in (0) do ( tasklist | find \"Projucer\" >nul & ( if errorlevel 1 ( "
  376. + targetFolder.getChildFile ("Projucer.exe").getFullPathName().quoted() + " & exit /b ) else ( timeout /t 10 >nul ) ) )\"";
  377. #endif
  378. if (newProcess.existsAsFile())
  379. {
  380. ChildProcess restartProcess;
  381. restartProcess.start (command, 0);
  382. ProjucerApplication::getApp().systemRequestedQuit();
  383. }
  384. }
  385. void LatestVersionCheckerAndUpdater::downloadAndInstall (const VersionInfo::Asset& asset, const File& targetFolder)
  386. {
  387. installer.reset (new DownloadAndInstallThread (asset, targetFolder,
  388. [this, targetFolder]
  389. {
  390. installer.reset();
  391. restartProcess (targetFolder);
  392. }));
  393. }
  394. //==============================================================================
  395. JUCE_IMPLEMENT_SINGLETON (LatestVersionCheckerAndUpdater)