From 05fbe78f5c3b30517f7152b37c157a99120682dc Mon Sep 17 00:00:00 2001 From: Tobias Markmann Date: Thu, 22 Sep 2016 17:07:33 +0200 Subject: Update and tidy up Sparkle software update support Sparkle is configured to do silent automatic background updates based on the provide appcast feed. When a new update was downloaded and is ready to be installed Swift notifies the user that they can restart to take advantage of the newly available version. Test-Information: Setup a custom appcast feed with a newer Swift dev release. Tested updating with Sparkle 1.14.0 binary release using DSA keys and signatures. Did not test Sparkle update with code signed Swift builds. Tested on macOS 10.12. Change-Id: Idad461ec53963c80990e51a502cb6e28bc7b6b4e diff --git a/BuildTools/SCons/SConscript.boot b/BuildTools/SCons/SConscript.boot index 8379a58..d6527f3 100644 --- a/BuildTools/SCons/SConscript.boot +++ b/BuildTools/SCons/SConscript.boot @@ -110,6 +110,9 @@ vars.Add("codesign_identity", "macOS code signing identity to be passed to codes vars.Add("signtool_key_pfx", "The keyfile (.pfx) that will be used to sign the Windows installer.", None) vars.Add("signtool_timestamp_url", "The timestamp server that will be queried for a signed time stamp in the signing process.", None) +# Automatic Software Update Options +vars.Add(PathVariable("sparkle_public_dsa_key", "Optional path to a public DSA key used to verify Sparkle software updates. Without specifiying this option, the app needs to be code signed for Sparkle to work.", None, PathVariable.PathIsFile)) + ################################################################################ # Set up default build & configure environment diff --git a/BuildTools/SCons/SConstruct b/BuildTools/SCons/SConstruct index b5757b8..2da3787 100644 --- a/BuildTools/SCons/SConstruct +++ b/BuildTools/SCons/SConstruct @@ -289,7 +289,7 @@ if env.get("try_gconf", True) and env["PLATFORM"] != "win32" and env["PLATFORM"] env["HAVE_SPARKLE"] = 0 if env["PLATFORM"] == "darwin" : sparkle_flags = { - "FRAMEWORKPATH": ["/Library/Frameworks"], + "FRAMEWORKPATH": ["3rdParty/Sparkle/Sparkle-1.14.0"], "FRAMEWORKS": ["Sparkle"] } sparkle_env = conf_env.Clone() @@ -298,9 +298,14 @@ if env["PLATFORM"] == "darwin" : if conf.CheckObjCHeader("Sparkle/Sparkle.h") : env["HAVE_SPARKLE"] = 1 env["SPARKLE_FLAGS"] = sparkle_flags - env["SPARKLE_FRAMEWORK"] = "/Library/Frameworks/Sparkle.framework" + env["SPARKLE_FRAMEWORK"] = Dir("../../3rdParty/Sparkle/Sparkle-1.14.0/Sparkle.framework") conf.Finish() + if env.get("sparkle_public_dsa_key", None) != None : + env["SWIFT_SPARKLE_PUBLIC_DSA_KEY"] = File(env.get("sparkle_public_dsa_key")) + else : + env["SWIFT_SPARKLE_PUBLIC_DSA_KEY"] = None + # Growl env["HAVE_GROWL"] = 0 if env["PLATFORM"] == "darwin" : diff --git a/BuildTools/SCons/Tools/AppBundle.py b/BuildTools/SCons/Tools/AppBundle.py index fda3484..5f19898 100644 --- a/BuildTools/SCons/Tools/AppBundle.py +++ b/BuildTools/SCons/Tools/AppBundle.py @@ -1,7 +1,7 @@ import SCons.Util, os.path def generate(env) : - def createAppBundle(env, bundle, version = "1.0", resources = [], frameworks = [], info = {}, handlesXMPPURIs = False) : + def createAppBundle(env, bundle, version = "1.0", resources = [], frameworks = [], info = {}, handlesXMPPURIs = False, sparklePublicDSAKey = None) : bundleDir = bundle + ".app" bundleContentsDir = bundleDir + "/Contents" resourcesDir = bundleContentsDir + "/Resources" @@ -44,6 +44,11 @@ def generate(env) : \n""" + + if sparklePublicDSAKey : + plist += "SUPublicDSAKeyFile" + plist += "" + sparklePublicDSAKey.name.encode("utf-8") + "" + env.Install(resourcesDir, sparklePublicDSAKey) plist += """ """ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 2b8ca99..3d9c1a7 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -68,6 +68,25 @@ Notes: paths - Currently only 32-bit builds of the Swift client are supported +## Automatic Software Updates +Automatic software updates allow distribution of updates directly to the end users. +This is useful for general feature updates, bug fixes and especially for security +updates. + +### Automatic Software Updates for Mac OS X using Sparkle +Swift supports integration with the software update framework [Sparkle](https://sparkle-project.org/) on OS X. For security reasons, +Sparkle requires the application to be either code-signed or a bundled public DSA +key. In case you do not code-sign, you can provide the path to the public DSA key +to be bundled with the application bundle via the `sparkle_public_dsa_key` SCons +argument. + +To build with Sparkle support, simply download Sparkle-1.14.0 and extract it to +`3rdParty/Sparkle/Sparkle-1.14.0`. SCons will pick it up during configuration +and build Swift with Sparkle support. + +The appcast URL is specified as a compile time preprocessor variable `SWIFT_APPCAST_URL` +in `Swift/QtUI/QtSwift.cpp` + ## Building Swiften for Android This section describes how to build Swiften for Android. It can then be used from any Android native code. This guide has been tested on OS X and Linux. diff --git a/SwifTools/AutoUpdater/AutoUpdater.h b/SwifTools/AutoUpdater/AutoUpdater.h index dec85c9..ed53e11 100644 --- a/SwifTools/AutoUpdater/AutoUpdater.h +++ b/SwifTools/AutoUpdater/AutoUpdater.h @@ -1,16 +1,27 @@ /* - * Copyright (c) 2010 Isode Limited. + * Copyright (c) 2010-2016 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #pragma once +#include + namespace Swift { class AutoUpdater { public: virtual ~AutoUpdater(); virtual void checkForUpdates() = 0; + virtual bool recommendRestartToUpdate() = 0; + + public: + /** + * Emit this signal if a new version of the software has been downloaded + * and the user needs to be notified so they can quit the app and start + * the newer version. + */ + boost::signals2::signal onSuggestRestartToUserToUpdate; }; } diff --git a/SwifTools/AutoUpdater/SparkleAutoUpdater.h b/SwifTools/AutoUpdater/SparkleAutoUpdater.h index 95ca35e..c3394f7 100644 --- a/SwifTools/AutoUpdater/SparkleAutoUpdater.h +++ b/SwifTools/AutoUpdater/SparkleAutoUpdater.h @@ -11,12 +11,19 @@ #include namespace Swift { + /** + * @brief The SparkleAutoUpdater class provides integration with Sparkle. + * This enables automatic silent background updates. If using this in Qt you + * need to emit a NSApplicationWillTerminateNotification before you quit + * the application. + */ class SparkleAutoUpdater : public AutoUpdater { public: SparkleAutoUpdater(const std::string& url); ~SparkleAutoUpdater(); void checkForUpdates(); + bool recommendRestartToUpdate(); private: class Private; diff --git a/SwifTools/AutoUpdater/SparkleAutoUpdater.mm b/SwifTools/AutoUpdater/SparkleAutoUpdater.mm index bcd1388..7e06b2f 100644 --- a/SwifTools/AutoUpdater/SparkleAutoUpdater.mm +++ b/SwifTools/AutoUpdater/SparkleAutoUpdater.mm @@ -1,13 +1,24 @@ +/* + * Copyright (c) 2016 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + #include #include #include +#include +#include + namespace Swift { class SparkleAutoUpdater::Private { public: SUUpdater* updater; + boost::intrusive_ptr delegate; + bool restartToUpdate = false; }; SparkleAutoUpdater::SparkleAutoUpdater(const std::string& url) { @@ -15,20 +26,36 @@ SparkleAutoUpdater::SparkleAutoUpdater(const std::string& url) { d->updater = [SUUpdater sharedUpdater]; [d->updater retain]; + + d->delegate = boost::intrusive_ptr([[SparkleAutoUpdaterDelegate alloc] init], false); + [d->delegate.get() setUpdateDownloadFinished: [&](){ + d->restartToUpdate = true; + onSuggestRestartToUserToUpdate(); + }]; + [d->updater setDelegate: d->delegate.get()]; + [d->updater setAutomaticallyChecksForUpdates: true]; + // Automatically check for an update after a day. + [d->updater setUpdateCheckInterval: 86400]; + [d->updater setAutomaticallyDownloadsUpdates: true]; - NSURL* nsurl = [NSURL URLWithString: - [NSString stringWithUTF8String: url.c_str()]]; + NSURL* nsurl = [NSURL URLWithString: std2NSString(url)]; [d->updater setFeedURL: nsurl]; } SparkleAutoUpdater::~SparkleAutoUpdater() { [d->updater release]; delete d; + SWIFT_LOG(warning) << std::endl; } void SparkleAutoUpdater::checkForUpdates() { + //[d->updater resetUpdateCycle]; // This is useful for testing to force a check ot start. [d->updater checkForUpdatesInBackground]; } +bool SparkleAutoUpdater::recommendRestartToUpdate() { + return d->restartToUpdate; +} + } diff --git a/SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.h b/SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.h new file mode 100644 index 0000000..8f408de --- /dev/null +++ b/SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2016 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#pragma once + +#include + +#import + +#import + +namespace Swift { + class SparkleAutoUpdater; +} + +@interface SparkleAutoUpdaterDelegate : NSObject +@property (atomic) std::function< void ()> updateDownloadFinished; + +- (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast; + +- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)update; + +- (id )versionComparatorForUpdater:(SUUpdater *)updater; + +- (void)updaterDidNotFindUpdate:(SUUpdater *)update; + +- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)update; + +- (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationInvocation:(NSInvocation *)invocation; + +- (void)updater:(SUUpdater *)updater didAbortWithError:(NSError *)error; +@end diff --git a/SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.mm b/SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.mm new file mode 100644 index 0000000..6e832ba --- /dev/null +++ b/SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.mm @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016 Isode Limited. + * All rights reserved. + * See the COPYING file for more information. + */ + +#import "SwifTools/AutoUpdater/SparkleAutoUpdaterDelegate.h" + +#include + +#include + +#include + +using namespace Swift; + +@implementation SparkleAutoUpdaterDelegate + +@synthesize updateDownloadFinished; + +- (void)updater:(SUUpdater *)updater didFinishLoadingAppcast:(SUAppcast *)appcast { + (void)updater; + (void)appcast; +} + +- (void)updater:(SUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)update { + (void)updater; + (void)update; +} + +- (id )versionComparatorForUpdater:(SUUpdater *)updater { + (void)updater; + return nil; +} + +- (void)updaterDidNotFindUpdate:(SUUpdater *)updater { + (void)updater; +} + +- (void)updater:(SUUpdater *)updater willInstallUpdate:(SUAppcastItem *)update { + (void)updater; + (void)update; +} + +- (void)updater:(SUUpdater *)updater willInstallUpdateOnQuit:(SUAppcastItem *)item immediateInstallationInvocation:(NSInvocation *)invocation { + (void)updater; + (void)item; + (void)invocation; + updateDownloadFinished(); +} + +- (void)updater:(SUUpdater *)updater didAbortWithError:(NSError *)error { + (void)updater; + SWIFT_LOG(error) << ns2StdString([error localizedDescription]) << std::endl; +} + +@end diff --git a/SwifTools/SConscript b/SwifTools/SConscript index aa6d47e..dec343e 100644 --- a/SwifTools/SConscript +++ b/SwifTools/SConscript @@ -50,7 +50,7 @@ if env["SCONS_STAGE"] == "build" : if swiftools_env.get("HAVE_SPARKLE", 0) : swiftools_env.UseFlags(swiftools_env["SPARKLE_FLAGS"]) swiftools_env.Append(CPPDEFINES = ["HAVE_SPARKLE"]) - sources += ["AutoUpdater/SparkleAutoUpdater.mm"] + sources += ["AutoUpdater/SparkleAutoUpdater.mm", "AutoUpdater/SparkleAutoUpdaterDelegate.mm"] if swiftools_env["PLATFORM"] == "win32" : sources += ["Idle/WindowsIdleQuerier.cpp"] diff --git a/Swift/QtUI/CocoaUIHelpers.h b/Swift/QtUI/CocoaUIHelpers.h index 58cd539..8d96bd9 100644 --- a/Swift/QtUI/CocoaUIHelpers.h +++ b/Swift/QtUI/CocoaUIHelpers.h @@ -21,6 +21,7 @@ namespace Swift { class CocoaUIHelpers { public: static void displayCertificateChainAsSheet(QWidget* parent, const std::vector& chain); + static void sendCocoaApplicationWillTerminateNotification(); }; } diff --git a/Swift/QtUI/CocoaUIHelpers.mm b/Swift/QtUI/CocoaUIHelpers.mm index c876312..3ffa72c 100644 --- a/Swift/QtUI/CocoaUIHelpers.mm +++ b/Swift/QtUI/CocoaUIHelpers.mm @@ -46,4 +46,8 @@ void CocoaUIHelpers::displayCertificateChainAsSheet(QWidget* parent, const std:: [certificates release]; } +void CocoaUIHelpers::sendCocoaApplicationWillTerminateNotification() { + [[NSNotificationCenter defaultCenter] postNotificationName:@"NSApplicationWillTerminateNotification" object:nil]; +} + } diff --git a/Swift/QtUI/QtSwift.cpp b/Swift/QtUI/QtSwift.cpp index 5d05a3d..d8dfac4 100644 --- a/Swift/QtUI/QtSwift.cpp +++ b/Swift/QtUI/QtSwift.cpp @@ -77,12 +77,16 @@ #include #endif +#if defined(SWIFTEN_PLATFORM_MACOSX) +#include +#endif + namespace Swift{ #if defined(SWIFTEN_PLATFORM_MACOSX) -//#define SWIFT_APPCAST_URL "http://swift.im/appcast/swift-mac-dev.xml" +#define SWIFT_APPCAST_URL "https://swift.im/appcast/swift-mac-dev.xml" #else -//#define SWIFT_APPCAST_URL "" +#define SWIFT_APPCAST_URL "" #endif po::options_description QtSwift::getOptionsDescription() { @@ -277,12 +281,14 @@ QtSwift::QtSwift(const po::variables_map& options) : networkFactories_(&clientMa mainControllers_.push_back(mainController); } + connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(handleAboutToQuit())); - // PlatformAutoUpdaterFactory autoUpdaterFactory; - // if (autoUpdaterFactory.isSupported()) { - // autoUpdater_ = autoUpdaterFactory.createAutoUpdater(SWIFT_APPCAST_URL); - // autoUpdater_->checkForUpdates(); - // } + PlatformAutoUpdaterFactory autoUpdaterFactory; + if (autoUpdaterFactory.isSupported()) { + autoUpdater_ = autoUpdaterFactory.createAutoUpdater(SWIFT_APPCAST_URL); + autoUpdater_->checkForUpdates(); + autoUpdater_->onSuggestRestartToUserToUpdate.connect(boost::bind(&QtSwift::handleRecommendRestartToInstallUpdate, this)); + } } QtSwift::~QtSwift() { @@ -312,4 +318,16 @@ QtSwift::~QtSwift() { delete applicationPathProvider_; } +void QtSwift::handleAboutToQuit() { +#if defined(SWIFTEN_PLATFORM_MACOSX) + // This is required so Sparkle knows about the application shutting down + // and can update the application in background. + CocoaUIHelpers::sendCocoaApplicationWillTerminateNotification(); +#endif +} + +void QtSwift::handleRecommendRestartToInstallUpdate() { + notifier_->showMessage(Notifier::SystemMessage, Q2PSTRING(tr("Swift Update Available")), Q2PSTRING(tr("Restart Swift now or later to update to the new Swift version")), "", [](){}); +} + } diff --git a/Swift/QtUI/QtSwift.h b/Swift/QtUI/QtSwift.h index 9932545..64b79b8 100644 --- a/Swift/QtUI/QtSwift.h +++ b/Swift/QtUI/QtSwift.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2015 Isode Limited. + * Copyright (c) 2010-2016 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ @@ -59,9 +59,15 @@ namespace Swift { QtSwift(const po::variables_map& options); static po::options_description getOptionsDescription(); ~QtSwift(); + + private slots: + void handleAboutToQuit(); + void handleRecommendRestartToInstallUpdate(); + private: XMLSettingsProvider* loadSettingsFile(const QString& fileName); void loadEmoticonsFile(const QString& fileName, std::map& emoticons); + private: QtEventLoop clientMainThreadCaller_; PlatformTLSFactories tlsFactories_; diff --git a/Swift/QtUI/SConscript b/Swift/QtUI/SConscript index 2d01672..fd47dd4 100644 --- a/Swift/QtUI/SConscript +++ b/Swift/QtUI/SConscript @@ -79,6 +79,9 @@ if env["PLATFORM"] == "win32" : myenv.Append(LIBS = "Cryptui") myenv.Append(CPPDEFINES = "HAVE_SCHANNEL") +if env["PLATFORM"] == "darwin" and env["HAVE_SPARKLE"] : + myenv.Append(LINKFLAGS = ["-Wl,-rpath,@loader_path/../Frameworks"]) + myenv.WriteVal("DefaultTheme.qrc", myenv.Value(generateQRCTheme(myenv.Dir("#/Swift/resources/themes/Default"), "Default"))) sources = [ @@ -364,7 +367,7 @@ if env["PLATFORM"] == "darwin" : if env["HAVE_GROWL"] : frameworks.append(env["GROWL_FRAMEWORK"]) commonResources[""] = commonResources.get("", []) + ["#/Swift/resources/MacOSX/Swift.icns"] - app = myenv.AppBundle("Swift", version = myenv["SWIFT_VERSION"], resources = commonResources, frameworks = frameworks, handlesXMPPURIs = True) + app = myenv.AppBundle("Swift", version = myenv["SWIFT_VERSION"], resources = commonResources, frameworks = frameworks, handlesXMPPURIs = True, sparklePublicDSAKey = myenv["SWIFT_SPARKLE_PUBLIC_DSA_KEY"]) if env["DIST"] : myenv.Command(["#/Packages/Swift/Swift-${SWIFT_VERSION}.dmg"], [app], ["Swift/Packaging/MacOSX/package.sh " + app.path + " Swift/Packaging/MacOSX/Swift.dmg.gz $TARGET $QTDIR " + "\"$CODE_SIGN_IDENTITY\""]) dsym = myenv.Command(["Swift-${SWIFT_VERSION}.dSYM"], ["Swift"], ["dsymutil -o ${TARGET} ${SOURCE}"]) -- cgit v0.10.2-6-g49f6