Updater: MacOS support

This commit is contained in:
TellowKrinkle 2022-12-05 00:55:17 -06:00 committed by refractionpcsx2
parent d7ef8a48fe
commit 8925da94e1
4 changed files with 202 additions and 12 deletions

View File

@ -15,6 +15,9 @@
#ifdef __APPLE__
#include <string>
#include <optional>
struct WindowInfo;
/// Helper functions for things that need Objective-C
@ -26,6 +29,12 @@ namespace CocoaTools
void AddThemeChangeHandler(void* ctx, void(handler)(void* ctx));
/// Remove a handler previously added using AddThemeChangeHandler with the given context
void RemoveThemeChangeHandler(void* ctx);
/// Get the bundle path to the actual application without any translocation fun
std::optional<std::string> GetNonTranslocatedBundlePath();
/// Move the given file to the trash, and return the path to its new location
std::optional<std::string> MoveToTrash(std::string_view file);
/// Launch the given application
bool LaunchApplication(std::string_view file);
}
#endif // __APPLE__

View File

@ -21,6 +21,8 @@
#include "Console.h"
#include "General.h"
#include "WindowInfo.h"
#include <dlfcn.h>
#include <mutex>
#include <vector>
#include <Cocoa/Cocoa.h>
#include <QuartzCore/QuartzCore.h>
@ -135,3 +137,65 @@ bool Common::PlaySoundAsync(const char* path)
NSSound* sound = [[NSSound alloc] initWithContentsOfFile:nspath byReference:YES];
return [sound play];
}
// MARK: - Updater
std::optional<std::string> CocoaTools::GetNonTranslocatedBundlePath()
{
// See https://objective-see.com/blog/blog_0x15.html
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (!url)
return std::nullopt;
if (void* handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY))
{
auto IsTranslocatedURL = reinterpret_cast<Boolean(*)(CFURLRef path, bool* isTranslocated, CFErrorRef*__nullable error)>(dlsym(handle, "SecTranslocateIsTranslocatedURL"));
auto CreateOriginalPathForURL = reinterpret_cast<CFURLRef __nullable(*)(CFURLRef translocatedPath, CFErrorRef*__nullable error)>(dlsym(handle, "SecTranslocateCreateOriginalPathForURL"));
bool is_translocated = false;
if (IsTranslocatedURL)
IsTranslocatedURL((__bridge CFURLRef)url, &is_translocated, nullptr);
if (is_translocated)
{
if (CFURLRef actual = CreateOriginalPathForURL((__bridge CFURLRef)url, nullptr))
url = (__bridge_transfer NSURL*)actual;
}
dlclose(handle);
}
return std::string([url fileSystemRepresentation]);
}
std::optional<std::string> CocoaTools::MoveToTrash(std::string_view file)
{
NSURL* url = [NSURL fileURLWithPath:[[NSString alloc] initWithBytes:file.data() length:file.size() encoding:NSUTF8StringEncoding]];
NSURL* new_url;
if (![[NSFileManager defaultManager] trashItemAtURL:url resultingItemURL:&new_url error:nil])
return std::nullopt;
return std::string([new_url fileSystemRepresentation]);
}
bool CocoaTools::LaunchApplication(std::string_view file)
{
NSURL* url = [NSURL fileURLWithPath:[[NSString alloc] initWithBytes:file.data() length:file.size() encoding:NSUTF8StringEncoding]];
if (@available(macOS 10.15, *))
{
// replacement api is async which isn't great for us
std::mutex done;
bool output;
done.lock();
NSWorkspaceOpenConfiguration* config = [NSWorkspaceOpenConfiguration new];
[config setCreatesNewApplicationInstance:YES];
[[NSWorkspace sharedWorkspace] openApplicationAtURL:url configuration:config completionHandler:[&](NSRunningApplication*_Nullable app, NSError*_Nullable error) {
output = app != nullptr;
done.unlock();
}];
done.lock();
done.unlock();
return output;
}
else
{
return [[NSWorkspace sharedWorkspace] launchApplicationAtURL:url options:NSWorkspaceLaunchNewInstance configuration:@{} error:nil];
}
}

View File

@ -26,6 +26,7 @@
#include "updater/UpdaterExtractor.h"
#include "common/CocoaTools.h"
#include "common/Console.h"
#include "common/FileSystem.h"
#include "common/StringUtil.h"
@ -47,7 +48,7 @@
// Logic to detect whether we can use the auto updater.
// We use tagged commit, because this gets set on nightly builds.
#if (defined(_WIN32) || defined(__linux__)) && (defined(GIT_TAGGED_COMMIT) && GIT_TAGGED_COMMIT)
#if (defined(_WIN32) || defined(__linux__) || defined(__APPLE__)) && (defined(GIT_TAGGED_COMMIT) && GIT_TAGGED_COMMIT)
#define AUTO_UPDATER_SUPPORTED 1
@ -55,6 +56,8 @@
#define UPDATE_PLATFORM_STR "Windows"
#elif defined(__linux__)
#define UPDATE_PLATFORM_STR "Linux"
#elif defined(__APPLE__)
#define UPDATE_PLATFORM_STR "MacOS"
#endif
#ifdef MULTI_ISA_SHARED_COMPILATION
@ -109,7 +112,7 @@ bool AutoUpdaterDialog::isSupported()
return true;
#else
// Windows - always supported.
// Windows, MacOS - always supported.
return true;
#endif
#else
@ -232,9 +235,10 @@ void AutoUpdaterDialog::getLatestReleaseComplete(QNetworkReply* reply)
is_symbols = true;
break;
}
else if (additional_tag_str == QStringLiteral("Qt"))
else if (additional_tag_str.startsWith(QStringLiteral("Qt")))
{
// found a qt build
// Note: The website improperly parses macOS file names, and gives them the tag "Qt.tar" instead of "Qt"
is_qt_asset = true;
}
else if (additional_tag_str == QStringLiteral("SSE4"))
@ -445,7 +449,7 @@ void AutoUpdaterDialog::downloadUpdateClicked()
return;
}
if (processUpdate(data))
if (processUpdate(data, progress))
progress.done(1);
else
progress.done(-1);
@ -511,7 +515,7 @@ void AutoUpdaterDialog::remindMeLaterClicked()
#if defined(_WIN32)
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog&)
{
const QString update_directory = QCoreApplication::applicationDirPath();
const QString update_zip_path = QStringLiteral("%1" FS_OSPATH_SEPARATOR_STR "%2").arg(update_directory).arg(UPDATER_ARCHIVE_NAME);
@ -582,7 +586,7 @@ bool AutoUpdaterDialog::doUpdate(const QString& zip_path, const QString& updater
#elif defined(__linux__)
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog&)
{
const char* appimage_path = std::getenv("APPIMAGE");
if (!appimage_path || !FileSystem::FileExists(appimage_path))
@ -658,9 +662,125 @@ bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
return true;
}
#elif defined(__APPLE__)
static QString UpdateVersionNumberInName(QString name, QStringView new_version)
{
QString current_version_string = QStringLiteral(GIT_TAG);
QStringView current_version = current_version_string;
if (!current_version.empty() && !new_version.empty() && current_version[0] == 'v' && new_version[0] == 'v')
{
current_version = current_version.mid(1);
new_version = new_version.mid(1);
}
if (!current_version.empty() && !new_version.empty())
name.replace(current_version.data(), current_version.size(), new_version.data(), new_version.size());
return name;
}
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog& progress)
{
std::optional<std::string> path = CocoaTools::GetNonTranslocatedBundlePath();
if (!path.has_value())
{
reportError("Couldn't get bundle path");
return false;
}
QFileInfo info(QString::fromStdString(*path));
if (!info.isBundle())
{
reportError("Application %s isn't a bundle", path->c_str());
return false;
}
if (info.suffix() != QStringLiteral("app"))
{
reportError("Unexpected application suffix %s on %s", info.suffix().toUtf8().constData(), path->c_str());
return false;
}
QString open_path;
{
QTemporaryDir temp_dir(info.path() + QStringLiteral("/PCSX2-UpdateStaging-XXXXXX"));
if (!temp_dir.isValid())
{
reportError("Failed to create update staging directory");
return false;
}
constexpr qsizetype chunk_size = 65536;
progress.setLabelText(QStringLiteral("Unpacking update..."));
progress.reset();
progress.setRange(0, static_cast<int>((update_data.size() + chunk_size - 1) / chunk_size));
QProcess untar;
untar.setProgram(QStringLiteral("/usr/bin/tar"));
untar.setArguments({QStringLiteral("xC"), temp_dir.path()});
untar.start();
for (qsizetype i = 0; i < update_data.size(); i += chunk_size)
{
progress.setValue(static_cast<int>(i / chunk_size));
const qsizetype amt = std::min(update_data.size() - i, chunk_size);
if (progress.wasCanceled() || untar.write(update_data.data() + i, amt) != amt)
{
if (!progress.wasCanceled())
reportError("Failed to unpack update (write stopped short)");
untar.closeWriteChannel();
if (!untar.waitForFinished(1000))
untar.kill();
return false;
}
}
untar.closeWriteChannel();
while (!untar.waitForFinished(1000))
{
if (progress.wasCanceled())
{
untar.kill();
return false;
}
}
progress.setValue(progress.maximum());
if (untar.exitCode() != EXIT_SUCCESS)
{
reportError("Failed to unpack update (tar exited with %u)", untar.exitCode());
return false;
}
QFileInfoList temp_dir_contents = QDir(temp_dir.path()).entryInfoList(QDir::Filter::Dirs | QDir::Filter::NoDotAndDotDot);
auto new_app = std::find_if(temp_dir_contents.begin(), temp_dir_contents.end(), [](const QFileInfo& file){ return file.suffix() == QStringLiteral("app"); });
if (new_app == temp_dir_contents.end())
{
reportError("Couldn't find application in update package");
return false;
}
QString new_name = UpdateVersionNumberInName(info.completeBaseName(), m_latest_version);
std::optional<std::string> trashed_path = CocoaTools::MoveToTrash(*path);
if (!trashed_path.has_value())
{
reportError("Failed to trash old application");
return false;
}
open_path = info.path() + QStringLiteral("/") + new_name + QStringLiteral(".app");
if (!QFile::rename(new_app->absoluteFilePath(), open_path))
{
QFile::rename(QString::fromStdString(*trashed_path), info.filePath());
reportError("Failed to move new application into place (couldn't rename '%s' to '%s')",
new_app->absoluteFilePath().toUtf8().constData(), open_path.toUtf8().constData());
return false;
}
QDir(QString::fromStdString(*trashed_path)).removeRecursively();
}
if (!CocoaTools::LaunchApplication(open_path.toStdString()))
{
reportError("Failed to start new application");
return false;
}
return true;
}
#else
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data, QProgressDialog& progress)
{
return false;
}

View File

@ -22,6 +22,7 @@
class QNetworkAccessManager;
class QNetworkReply;
class QProgressDialog;
class AutoUpdaterDialog final : public QDialog
{
@ -58,13 +59,9 @@ private:
void checkIfUpdateNeeded();
QString getCurrentUpdateTag() const;
bool processUpdate(const QByteArray& update_data, QProgressDialog& progress);
#if defined(_WIN32)
bool processUpdate(const QByteArray& update_data);
bool doUpdate(const QString& zip_path, const QString& updater_path, const QString& destination_path);
#elif defined(__linux__)
bool processUpdate(const QByteArray& update_data);
#else
bool processUpdate(const QByteArray& update_data);
#endif
Ui::AutoUpdaterDialog m_ui;