From 12a27d8b99473d00d8939a78601c3ebcd6b2a2a9 Mon Sep 17 00:00:00 2001 From: derrod Date: Sat, 5 Nov 2022 22:32:43 +0100 Subject: [PATCH] UI: Add update channels (macOS) --- UI/CMakeLists.txt | 23 +++--- UI/obs-app.cpp | 14 +++- UI/sparkle-updater.mm | 141 ----------------------------------- UI/update/mac-update.cpp | 87 +++++++++++++++++++++ UI/update/mac-update.hpp | 51 +++++++++++++ UI/update/sparkle-updater.mm | 95 +++++++++++++++++++++++ UI/window-basic-main.cpp | 51 +++++++++---- UI/window-basic-main.hpp | 1 + UI/window-basic-settings.cpp | 14 ++-- 9 files changed, 301 insertions(+), 176 deletions(-) delete mode 100644 UI/sparkle-updater.mm create mode 100644 UI/update/mac-update.cpp create mode 100644 UI/update/mac-update.hpp create mode 100644 UI/update/sparkle-updater.mm diff --git a/UI/CMakeLists.txt b/UI/CMakeLists.txt index 858817a49..0941dc085 100644 --- a/UI/CMakeLists.txt +++ b/UI/CMakeLists.txt @@ -430,16 +430,6 @@ elseif(OS_MACOS) target_link_libraries(obs PRIVATE ${APPKIT} ${AVFOUNDATION} ${APPLICATIONSERVICES}) - if(ENABLE_SPARKLE_UPDATER) - find_library(SPARKLE Sparkle) - mark_as_advanced(SPARKLE) - - target_sources(obs PRIVATE sparkle-updater.mm) - target_compile_definitions(obs PRIVATE ENABLE_SPARKLE_UPDATER) - - target_link_libraries(obs PRIVATE ${SPARKLE}) - endif() - target_sources(obs PRIVATE platform-osx.mm) target_sources(obs PRIVATE forms/OBSPermissions.ui window-permissions.cpp window-permissions.hpp) @@ -454,6 +444,19 @@ elseif(OS_MACOS) PRIVATE update/crypto-helpers.hpp update/crypto-helpers-mac.mm update/shared-update.cpp update/shared-update.hpp update/update-helpers.cpp update/update-helpers.hpp) + + if(ENABLE_SPARKLE_UPDATER) + find_library(SPARKLE Sparkle) + mark_as_advanced(SPARKLE) + + target_sources(obs PRIVATE update/mac-update.cpp update/mac-update.hpp + update/sparkle-updater.mm) + target_compile_definitions(obs PRIVATE ENABLE_SPARKLE_UPDATER) + target_link_libraries(obs PRIVATE ${SPARKLE}) + # Enable Automatic Reference Counting for Sparkle wrapper + set_source_files_properties(update/sparkle-updater.mm + PROPERTIES COMPILE_FLAGS -fobjc-arc) + endif() endif() set_source_files_properties(platform-osx.mm PROPERTIES COMPILE_FLAGS diff --git a/UI/obs-app.cpp b/UI/obs-app.cpp index a45624a1d..0deda133b 100644 --- a/UI/obs-app.cpp +++ b/UI/obs-app.cpp @@ -54,7 +54,6 @@ #include #ifdef _WIN32 -#include #include #include #else @@ -62,6 +61,10 @@ #include #endif +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) +#include +#endif + #if !defined(_WIN32) && !defined(__APPLE__) #include #include @@ -1268,7 +1271,7 @@ bool OBSApp::InitTheme() return SetTheme("System"); } -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) void ParseBranchesJson(const std::string &jsonString, vector &out, std::string &error) { @@ -1281,6 +1284,9 @@ void ParseBranchesJson(const std::string &jsonString, vector &out, #ifdef _WIN32 if (!item["windows"].bool_value()) continue; +#elif defined(__APPLE__) + if (!item["macos"].bool_value()) + continue; #endif UpdateBranch branch = { @@ -1329,7 +1335,7 @@ fail: void OBSApp::SetBranchData(const string &data) { -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) string error; vector result; @@ -1356,7 +1362,7 @@ std::vector OBSApp::GetBranches() /* Always ensure the default branch exists */ out.push_back(UpdateBranch{"stable", "", "", true, true}); -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) if (!branches_loaded) { vector result; if (LoadBranchesFile(result)) diff --git a/UI/sparkle-updater.mm b/UI/sparkle-updater.mm deleted file mode 100644 index 932a3a705..000000000 --- a/UI/sparkle-updater.mm +++ /dev/null @@ -1,141 +0,0 @@ -#import -#import - -static inline bool equali(NSString *a, NSString *b) -{ - return a && b && [a caseInsensitiveCompare:b] == NSOrderedSame; -} - -@interface OBSSparkleUpdateDelegate - : NSObject { -} -@property (nonatomic) bool updateToUndeployed; -@end - -@implementation OBSSparkleUpdateDelegate { -} - -@synthesize updateToUndeployed; - -- (SUAppcastItem *)bestValidUpdateWithDeltasInAppcast:(SUAppcast *)appcast - forUpdater:(SUUpdater *)updater -{ - SUAppcastItem *item = appcast.items.firstObject; - if (!appcast.items.firstObject) - return nil; - - SUAppcastItem *app = nil, *mpkg = nil; - for (SUAppcastItem *item in appcast.items) { - NSString *deployed = item.propertiesDictionary[@"ce:deployed"]; - if (deployed && !(deployed.boolValue || updateToUndeployed)) - continue; - - NSString *type = item.propertiesDictionary[@"ce:packageType"]; - if (!mpkg && (!type || equali(type, @"mpkg"))) - mpkg = item; - else if (!app && type && equali(type, @"app")) - app = item; - - if (app && mpkg) - break; - } - - if (app) - item = app; - - NSBundle *host = updater.hostBundle; - if (mpkg && (!app || equali(host.bundlePath, @"/Applications/OBS.app"))) - item = mpkg; - - NSMutableDictionary *dict = [NSMutableDictionary - dictionaryWithDictionary:item.propertiesDictionary]; - NSString *build = [host objectForInfoDictionaryKey:@"CFBundleVersion"]; - NSString *url = dict[@"sparkle:releaseNotesLink"]; - dict[@"sparkle:releaseNotesLink"] = - [url stringByAppendingFormat:@"#%@", build]; - - return [[SUAppcastItem alloc] initWithDictionary:dict]; -} - -- (SUAppcastItem *)bestValidUpdateInAppcast:(SUAppcast *)appcast - forUpdater:(SUUpdater *)updater -{ - SUAppcastItem *selected = [self - bestValidUpdateWithDeltasInAppcast:appcast - forUpdater:updater]; - - NSBundle *host = updater.hostBundle; - NSString *build = [host objectForInfoDictionaryKey:@"CFBundleVersion"]; - SUAppcastItem *deltaUpdate = [selected deltaUpdates][build]; - if (deltaUpdate) - return deltaUpdate; - - return selected; -} - -- (NSString *)feedURLStringForUpdater:(SUUpdater *)updater -{ - //URL from Info.plist takes precedence because there may be bundles with - //differing feed URLs on the system - NSBundle *bundle = updater.hostBundle; - return [bundle objectForInfoDictionaryKey:@"SUFeedURL"]; -} - -- (NSComparisonResult)compareVersion:(NSString *)versionA - toVersion:(NSString *)versionB -{ - if (![versionA isEqual:versionB]) - return NSOrderedAscending; - return NSOrderedSame; -} - -- (id)versionComparatorForUpdater:(SUUpdater *)__unused - updater -{ - return self; -} - -@end - -static inline bool bundle_matches(NSBundle *bundle) -{ - if (!bundle.executablePath) - return false; - - NSRange r = [bundle.executablePath rangeOfString:@"Contents/MacOS/"]; - return [bundle.bundleIdentifier isEqual:@"com.obsproject.obs-studio"] && - r.location != NSNotFound; -} - -static inline NSBundle *find_bundle() -{ - NSFileManager *fm = [NSFileManager defaultManager]; - NSString *path = [fm currentDirectoryPath]; - NSString *prev = path; - do { - NSBundle *bundle = [NSBundle bundleWithPath:path]; - if (bundle_matches(bundle)) - return bundle; - - prev = path; - path = [path stringByDeletingLastPathComponent]; - } while (![prev isEqual:path]); - return nil; -} - -static SUUpdater *updater; - -static OBSSparkleUpdateDelegate *delegate; - -void init_sparkle_updater(bool update_to_undeployed) -{ - updater = [SUUpdater updaterForBundle:find_bundle()]; - delegate = [[OBSSparkleUpdateDelegate alloc] init]; - delegate.updateToUndeployed = update_to_undeployed; - updater.delegate = delegate; -} - -void trigger_sparkle_update() -{ - [updater checkForUpdates:nil]; -} diff --git a/UI/update/mac-update.cpp b/UI/update/mac-update.cpp new file mode 100644 index 000000000..717fb0c75 --- /dev/null +++ b/UI/update/mac-update.cpp @@ -0,0 +1,87 @@ +#include "update-helpers.hpp" +#include "shared-update.hpp" +#include "qt-wrappers.hpp" +#include "mac-update.hpp" +#include "obs-app.hpp" + +#include + +#include + +/* ------------------------------------------------------------------------ */ + +#ifndef MAC_BRANCHES_URL +#define MAC_BRANCHES_URL "https://obsproject.com/update_studio/branches.json" +#endif + +#ifndef MAC_DEFAULT_BRANCH +#define MAC_DEFAULT_BRANCH "stable" +#endif + +/* ------------------------------------------------------------------------ */ + +bool GetBranch(std::string &selectedBranch) +{ + const char *config_branch = + config_get_string(GetGlobalConfig(), "General", "UpdateBranch"); + if (!config_branch) + return true; + + bool found = false; + for (const UpdateBranch &branch : App()->GetBranches()) { + if (branch.name != config_branch) + continue; + /* A branch that is found but disabled will just silently fall back to + * the default. But if the branch was removed entirely, the user should + * be warned, so leave this false *only* if the branch was removed. */ + found = true; + + if (branch.is_enabled) { + selectedBranch = branch.name.toStdString(); + } + break; + } + + return found; +} + +/* ------------------------------------------------------------------------ */ + +void MacUpdateThread::infoMsg(const QString &title, const QString &text) +{ + OBSMessageBox::information(App()->GetMainWindow(), title, text); +} + +void MacUpdateThread::info(const QString &title, const QString &text) +{ + QMetaObject::invokeMethod(this, "infoMsg", Qt::BlockingQueuedConnection, + Q_ARG(QString, title), Q_ARG(QString, text)); +} + +void MacUpdateThread::run() +try { + std::string text; + std::string branch = MAC_DEFAULT_BRANCH; + + /* ----------------------------------- * + * get branches from server */ + + if (FetchAndVerifyFile("branches", "obs-studio/updates/branches.json", + MAC_BRANCHES_URL, &text)) + App()->SetBranchData(text); + + /* ----------------------------------- * + * Validate branch selection */ + + if (!GetBranch(branch)) { + config_set_string(GetGlobalConfig(), "General", "UpdateBranch", + MAC_DEFAULT_BRANCH); + info(QTStr("Updater.BranchNotFound.Title"), + QTStr("Updater.BranchNotFound.Text")); + } + + emit Result(QString::fromStdString(branch), manualUpdate); + +} catch (std::string &text) { + blog(LOG_WARNING, "%s: %s", __FUNCTION__, text.c_str()); +} diff --git a/UI/update/mac-update.hpp b/UI/update/mac-update.hpp new file mode 100644 index 000000000..cedac60eb --- /dev/null +++ b/UI/update/mac-update.hpp @@ -0,0 +1,51 @@ +#ifndef MAC_UPDATER_H +#define MAC_UPDATER_H + +#include + +#include +#include +#include + +class QAction; + +class MacUpdateThread : public QThread { + Q_OBJECT + + bool manualUpdate; + + virtual void run() override; + + void info(const QString &title, const QString &text); + +signals: + void Result(const QString &branch, bool manual); + +private slots: + void infoMsg(const QString &title, const QString &text); + +public: + MacUpdateThread(bool manual) : manualUpdate(manual) {} +}; + +#ifdef __OBJC__ +@class OBSUpdateDelegate; +#endif + +class OBSSparkle : public QObject { + Q_OBJECT + +public: + OBSSparkle(const char *branch, QAction *checkForUpdatesAction); + void setBranch(const char *branch); + void checkForUpdates(bool manualCheck); + +private: +#ifdef __OBJC__ + OBSUpdateDelegate *updaterDelegate; +#else + void *updaterDelegate; +#endif +}; + +#endif diff --git a/UI/update/sparkle-updater.mm b/UI/update/sparkle-updater.mm new file mode 100644 index 000000000..ce253d1b6 --- /dev/null +++ b/UI/update/sparkle-updater.mm @@ -0,0 +1,95 @@ +#include "mac-update.hpp" + +#include + +#import +#import + +@interface OBSUpdateDelegate : NSObject { +} +@property (copy) NSString *branch; +@property (nonatomic) SPUStandardUpdaterController *updaterController; +@end + +@implementation OBSUpdateDelegate { +} + +@synthesize branch; + +- (nonnull NSSet *)allowedChannelsForUpdater: + (nonnull SPUUpdater *)updater +{ + return [NSSet setWithObject:branch]; +} + +- (void)observeCanCheckForUpdatesWithAction:(QAction *)action +{ + [_updaterController.updater + addObserver:self + forKeyPath:NSStringFromSelector(@selector(canCheckForUpdates)) + options:(NSKeyValueObservingOptionInitial | + NSKeyValueObservingOptionNew) + context:(void *)action]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context +{ + if ([keyPath isEqualToString:NSStringFromSelector( + @selector(canCheckForUpdates))]) { + QAction *menuAction = (QAction *)context; + menuAction->setEnabled( + _updaterController.updater.canCheckForUpdates); + } else { + [super observeValueForKeyPath:keyPath + ofObject:object + change:change + context:context]; + } +} + +- (void)dealloc +{ + @autoreleasepool { + [_updaterController.updater + removeObserver:self + forKeyPath:NSStringFromSelector( + @selector(canCheckForUpdates))]; + } +} + +@end + +OBSSparkle::OBSSparkle(const char *branch, QAction *checkForUpdatesAction) +{ + @autoreleasepool { + updaterDelegate = [[OBSUpdateDelegate alloc] init]; + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; + updaterDelegate.updaterController = + [[SPUStandardUpdaterController alloc] + initWithStartingUpdater:YES + updaterDelegate:updaterDelegate + userDriverDelegate:nil]; + [updaterDelegate observeCanCheckForUpdatesWithAction: + checkForUpdatesAction]; + } +} + +void OBSSparkle::setBranch(const char *branch) +{ + updaterDelegate.branch = [NSString stringWithUTF8String:branch]; +} + +void OBSSparkle::checkForUpdates(bool manualCheck) +{ + @autoreleasepool { + if (manualCheck) { + [updaterDelegate.updaterController checkForUpdates:nil]; + } else { + [updaterDelegate.updaterController + .updater checkForUpdatesInBackground]; + } + } +} diff --git a/UI/window-basic-main.cpp b/UI/window-basic-main.cpp index bd1d30686..b0f19643d 100644 --- a/UI/window-basic-main.cpp +++ b/UI/window-basic-main.cpp @@ -86,6 +86,10 @@ #include "update/shared-update.hpp" #endif +#ifdef ENABLE_SPARKLE_UPDATER +#include "update/mac-update.hpp" +#endif + #include "ui_OBSBasic.h" #include "ui_ColorSelect.h" @@ -2041,7 +2045,8 @@ void OBSBasic::OBSInit() QMetaObject::invokeMethod(this, "on_autoConfigure_triggered", Qt::QueuedConnection); -#if defined(_WIN32) && (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) +#if (defined(_WIN32) || defined(__APPLE__)) && \ + (OBS_RELEASE_CANDIDATE > 0 || OBS_BETA > 0) /* Automatically set branch to "beta" the first time a pre-release build is run. */ if (!config_get_bool(App()->GlobalConfig(), "General", "AutoBetaOptIn")) { @@ -3720,11 +3725,6 @@ bool OBSBasic::QueryRemoveSource(obs_source_t *source) #define UPDATE_CHECK_INTERVAL (60 * 60 * 24 * 4) /* 4 days */ -#if defined(ENABLE_SPARKLE_UPDATER) -void init_sparkle_updater(bool update_to_undeployed); -void trigger_sparkle_update(); -#endif - void OBSBasic::TimedCheckForUpdates() { if (App()->IsUpdaterDisabled()) @@ -3734,8 +3734,7 @@ void OBSBasic::TimedCheckForUpdates() return; #if defined(ENABLE_SPARKLE_UPDATER) - init_sparkle_updater(config_get_bool(App()->GlobalConfig(), "General", - "UpdateToUndeployed")); + CheckForUpdates(false); #elif _WIN32 long long lastUpdate = config_get_int(App()->GlobalConfig(), "General", "LastUpdateCheck"); @@ -3758,22 +3757,48 @@ void OBSBasic::TimedCheckForUpdates() void OBSBasic::CheckForUpdates(bool manualUpdate) { -#if defined(ENABLE_SPARKLE_UPDATER) - trigger_sparkle_update(); -#elif _WIN32 +#if _WIN32 ui->actionCheckForUpdates->setEnabled(false); ui->actionRepair->setEnabled(false); if (updateCheckThread && updateCheckThread->isRunning()) return; - updateCheckThread.reset(new AutoUpdateThread(manualUpdate)); updateCheckThread->start(); -#endif +#elif defined(ENABLE_SPARKLE_UPDATER) + ui->actionCheckForUpdates->setEnabled(false); + if (updateCheckThread && updateCheckThread->isRunning()) + return; + + MacUpdateThread *mut = new MacUpdateThread(manualUpdate); + connect(mut, &MacUpdateThread::Result, this, + &OBSBasic::MacBranchesFetched, Qt::QueuedConnection); + updateCheckThread.reset(mut); + updateCheckThread->start(); +#endif UNUSED_PARAMETER(manualUpdate); } +void OBSBasic::MacBranchesFetched(const QString &branch, bool manualUpdate) +{ +#ifdef ENABLE_SPARKLE_UPDATER + static OBSSparkle *updater; + + if (!updater) { + updater = new OBSSparkle(QT_TO_UTF8(branch), + ui->actionCheckForUpdates); + return; + } + + updater->setBranch(QT_TO_UTF8(branch)); + updater->checkForUpdates(manualUpdate); +#else + UNUSED_PARAMETER(branch); + UNUSED_PARAMETER(manualUpdate); +#endif +} + void OBSBasic::updateCheckFinished() { ui->actionCheckForUpdates->setEnabled(true); diff --git a/UI/window-basic-main.hpp b/UI/window-basic-main.hpp index 7f096b182..e2c72ddbd 100644 --- a/UI/window-basic-main.hpp +++ b/UI/window-basic-main.hpp @@ -541,6 +541,7 @@ private: obs_data_array_t *SaveProjectors(); void LoadSavedProjectors(obs_data_array_t *savedProjectors); + void MacBranchesFetched(const QString &branch, bool manualUpdate); void ReceivedIntroJson(const QString &text); void ShowWhatsNew(const QString &url); diff --git a/UI/window-basic-settings.cpp b/UI/window-basic-settings.cpp index 0ce8800ab..e51d88ce3 100644 --- a/UI/window-basic-settings.cpp +++ b/UI/window-basic-settings.cpp @@ -583,9 +583,6 @@ OBSBasicSettings::OBSBasicSettings(QWidget *parent) ui->updateChannelBox = nullptr; delete ui->updateSettingsGroupBox; ui->updateSettingsGroupBox = nullptr; -#elif defined(__APPLE__) - delete ui->updateChannelBox; - ui->updateChannelBox = nullptr; delete ui->updateChannelLabel; ui->updateChannelLabel = nullptr; #else @@ -1207,7 +1204,7 @@ void OBSBasicSettings::LoadThemeList() ui->theme->setCurrentIndex(idx); } -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) void TranslateBranchInfo(const QString &name, QString &displayName, QString &description) { @@ -1225,7 +1222,7 @@ void TranslateBranchInfo(const QString &name, QString &displayName, void OBSBasicSettings::LoadBranchesList() { -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) bool configBranchRemoved = true; QString configBranch = config_get_string(GetGlobalConfig(), "General", "UpdateBranch"); @@ -1286,7 +1283,7 @@ void OBSBasicSettings::LoadGeneralSettings() "EnableAutoUpdates"); ui->enableAutoUpdates->setChecked(enableAutoUpdates); -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) LoadBranchesList(); #endif #endif @@ -3126,7 +3123,7 @@ void OBSBasicSettings::SaveGeneralSettings() "EnableAutoUpdates", ui->enableAutoUpdates->isChecked()); #endif -#ifdef _WIN32 +#if defined(_WIN32) || defined(ENABLE_SPARKLE_UPDATER) int branchIdx = ui->updateChannelBox->currentIndex(); QString branchName = ui->updateChannelBox->itemData(branchIdx).toString(); @@ -3136,7 +3133,8 @@ void OBSBasicSettings::SaveGeneralSettings() QT_TO_UTF8(branchName)); forceUpdateCheck = true; } - +#endif +#ifdef _WIN32 if (ui->hideOBSFromCapture && WidgetChanged(ui->hideOBSFromCapture)) { bool hide_window = ui->hideOBSFromCapture->isChecked(); config_set_bool(GetGlobalConfig(), "BasicWindow",