Large diffs are not rendered by default.

@@ -0,0 +1,63 @@
set(STORYBOARDS Main.storyboard)

set(SOURCES
main.m
AppDelegate.h
AppDelegate.mm
ViewController.h
ViewController.m
UI.h
UI.mm
${STORYBOARDS}
)

add_executable(MacUpdater ${SOURCES})

set(MacUpdater_NAME "Dolphin Updater")

set_target_properties(MacUpdater PROPERTIES
MACOSX_BUNDLE true
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
OUTPUT_NAME ${MacUpdater_NAME})

target_compile_options(MacUpdater PRIVATE -x objective-c++)

# Copy icon into the bundle
target_sources(MacUpdater PRIVATE "${CMAKE_SOURCE_DIR}/Data/Dolphin.icns")
set_source_files_properties("${CMAKE_SOURCE_DIR}/Data/Dolphin.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources)

target_link_libraries(MacUpdater PRIVATE
"-framework Cocoa"
"-framework AppKit"
"-framework CoreData"
"-framework Foundation"
uicommon
mbedtls
z
ed25519
)

# Compile storyboards (Adapted from https://gitlab.kitware.com/cmake/community/wikis/doc/tutorials/OSX-InterfaceBuilderFiles)

# Make sure we can find the 'ibtool' program. If we can NOT find it we
# skip generation of this project
find_program(IBTOOL ibtool HINTS "/usr/bin" "${OSX_DEVELOPER_ROOT}/usr/bin")
if (${IBTOOL} STREQUAL "IBTOOL-NOTFOUND")
message(SEND_ERROR "ibtool can not be found and is needed to compile the .storyboard files. It should have been installed with
the Apple developer tools. The default system paths were searched in addition to ${OSX_DEVELOPER_ROOT}/usr/bin")
endif()

foreach(sb ${STORYBOARDS})
set(MacUpdater_BIN_DIR ${CMAKE_BINARY_DIR}/Binaries)

if (CMAKE_GENERATOR STREQUAL Xcode)
string(APPEND MacUpdater_BIN_DIR "/\${CONFIGURATION}")
endif()

add_custom_command(TARGET MacUpdater POST_BUILD
COMMAND ${IBTOOL} --errors --warnings --notices --output-format human-readable-text
--compile ${MacUpdater_BIN_DIR}/${MacUpdater_NAME}.app/Contents/Resources/${sb}c
${CMAKE_CURRENT_SOURCE_DIR}/${sb}
COMMENT "Compiling Storyboard ${sb}...")
endforeach()

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>${MacUpdater_NAME}</string>
<key>CFBundleIconFile</key>
<string>Dolphin.icns</string>
<key>CFBundleIdentifier</key>
<string>com.dolphinteam.dolphin-updater</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Updater</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSMinimumSystemVersion</key>
<string>10.9</string>
<key>NSHumanReadableCopyright</key>
<string>Licensed under GPL version 2 or later (GPLv2+)</string>
<key>NSMainStoryboardFile</key>
<string>Main</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment version="101000" identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Application-->
<scene sceneID="JPo-4y-FX3">
<objects>
<application id="hnw-xV-0zn" sceneMemberID="viewController">
<menu key="mainMenu" title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
<items>
<menuItem title="Updater" id="1Xt-HY-uBw">
<modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Updater" systemMenu="apple" id="uQy-DD-JDr">
<items>
<menuItem title="About Updater" id="5kV-Vb-QxS">
<modifierMask key="keyEquivalentModifierMask"/>
<connections>
<action selector="orderFrontStandardAboutPanel:" target="Ady-hI-5gd" id="Exp-CZ-Vem"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Quit Updater" keyEquivalent="q" id="4sb-4s-VLi">
<connections>
<action selector="terminate:" target="Ady-hI-5gd" id="Te7-pn-YzF"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
</items>
</menu>
<connections>
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
</connections>
</application>
<customObject id="Voe-Tx-rLC" customClass="AppDelegate"/>
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="Dolphin Updater" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="IQv-IB-iLA">
<windowStyleMask key="styleMask" titled="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
<connections>
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
</connections>
</window>
<connections>
<segue destination="XfG-lQ-9wD" kind="relationship" relationship="window.shadowedContentViewController" id="cq2-FE-JQM"/>
</connections>
</windowController>
<customObject id="Oky-zY-oP4" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="250"/>
</scene>
<!--View Controller-->
<scene sceneID="hIz-AP-VOD">
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" sceneMemberID="viewController">
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
<rect key="frame" x="0.0" y="0.0" width="480" height="120"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<progressIndicator identifier="progressTotal" wantsLayer="YES" fixedFrame="YES" maxValue="100" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="oLS-AW-V72">
<rect key="frame" x="20" y="81" width="440" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<progressIndicator identifier="progressCurrent" wantsLayer="YES" fixedFrame="YES" maxValue="100" indeterminate="YES" style="bar" translatesAutoresizingMaskIntoConstraints="NO" id="mni-rL-166">
<rect key="frame" x="20" y="39" width="440" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<textField identifier="labelProgress" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="r87-pn-Exw" userLabel="Label">
<rect key="frame" x="18" y="15" width="444" height="17"/>
<autoresizingMask key="autoresizingMask"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Initializing..." id="FBK-Tz-5cA">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<action selector="progressLabel:" target="XfG-lQ-9wD" id="PDs-H5-idS"/>
</connections>
</textField>
</subviews>
</view>
<connections>
<outlet property="foo" destination="r87-pn-Exw" id="i76-Sg-K3o"/>
<outlet property="labelProgress" destination="r87-pn-Exw" id="v6b-r7-3lR"/>
<outlet property="progressCurrent" destination="mni-rL-166" id="A3C-wp-83j"/>
<outlet property="progressTotal" destination="oLS-AW-V72" id="pwo-CZ-XyN"/>
</connections>
</viewController>
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="647"/>
</scene>
</scenes>
</document>
@@ -0,0 +1,24 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#pragma once

#include <string>

namespace UI
{
void Error(const std::string& text);

void SetVisible(bool visible);

void SetDescription(const std::string& text);

void SetTotalMarquee(bool marquee);
void ResetTotalProgress();
void SetTotalProgress(int current, int total);

void SetCurrentMarquee(bool marquee);
void ResetCurrentProgress();
void SetCurrentProgress(int current, int total);
} // namespace UI
@@ -0,0 +1,107 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include "MacUpdater/UI.h"
#include "MacUpdater/ViewController.h"

#include <Cocoa/Cocoa.h>

#include <functional>

// When we call from the main thread, we are not allowed to use
// dispatch_sync(dispatch_get_main_queue() as it will cause crashes) To prevent this check if we're
// already on the main thread first
void run_on_main(std::function<void()> fnc)
{
if (![NSThread isMainThread])
{
dispatch_sync(dispatch_get_main_queue(), ^{
fnc();
});
}
else
{
fnc();
}
}

NSWindow* GetWindow()
{
return [[[NSApplication sharedApplication] windows] objectAtIndex:0];
}

ViewController* GetView()
{
return (ViewController*)GetWindow().contentViewController;
}

void UI::Error(const std::string& text)
{
run_on_main([&] {
NSAlert* alert = [[[NSAlert alloc] init] autorelease];

[alert setMessageText:@"Fatal error"];
[alert
setInformativeText:[NSString stringWithCString:text.c_str() encoding:NSUTF8StringEncoding]];
[alert setAlertStyle:NSAlertStyleCritical];

[alert beginSheetModalForWindow:GetWindow()
completionHandler:^(NSModalResponse) {
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
}];
});
}

void UI::SetVisible(bool visible)
{
run_on_main([&] {
if (visible)
{
[NSApp unhide:nil];
[NSApp activateIgnoringOtherApps:YES];
}
else
{
[NSApp hide:nil];
}
});
}

void UI::SetDescription(const std::string& text)
{
run_on_main([&] {
[GetView()
SetDescription:[NSString stringWithCString:text.c_str() encoding:NSUTF8StringEncoding]];
});
}

void UI::SetTotalMarquee(bool marquee)
{
run_on_main([marquee] { [GetView() SetTotalMarquee:marquee]; });
}

void UI::SetCurrentMarquee(bool marquee)
{
run_on_main([&] { [GetView() SetCurrentMarquee:marquee]; });
}

void UI::ResetTotalProgress()
{
run_on_main([] { SetTotalProgress(0, 1); });
}

void UI::ResetCurrentProgress()
{
run_on_main([] { SetCurrentProgress(0, 1); });
}

void UI::SetCurrentProgress(int current, int total)
{
run_on_main([&] { [GetView() SetCurrentProgress:(double)current total:(double)total]; });
}

void UI::SetTotalProgress(int current, int total)
{
run_on_main([&] { [GetView() SetTotalProgress:(double)current total:(double)total]; });
}
@@ -0,0 +1,21 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#import <Cocoa/Cocoa.h>

@interface ViewController : NSViewController

@property(assign) IBOutlet NSProgressIndicator* progressCurrent;
@property(assign) IBOutlet NSProgressIndicator* progressTotal;
@property(assign) IBOutlet NSTextField* labelProgress;

- (void)SetDescription:(NSString*)string;

- (void)SetTotalMarquee:(bool)marquee;
- (void)SetCurrentMarquee:(bool)marquee;

- (void)SetTotalProgress:(double)current total:(double)total;
- (void)SetCurrentProgress:(double)current total:(double)total;

@end
@@ -0,0 +1,52 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#import "ViewController.h"

@implementation ViewController

- (void)viewDidLoad
{
[super viewDidLoad];
NSWindow* window = [[self view] window];

[window setLevel:kCGMainMenuWindowLevel - 1];
[window setCollectionBehavior:NSWindowCollectionBehaviorStationary |
NSWindowCollectionBehaviorCanJoinAllSpaces |
NSWindowCollectionBehaviorFullScreenAuxiliary];
}

- (void)SetDescription:(NSString*)string
{
[_labelProgress setStringValue:string];
}

- (void)SetTotalMarquee:(bool)marquee
{
[_progressTotal setIndeterminate:marquee];
}

- (void)SetCurrentMarquee:(bool)marquee
{
[_progressCurrent setIndeterminate:marquee];
}

- (void)SetTotalProgress:(double)current total:(double)total
{
[_progressTotal setMaxValue:total];
[_progressTotal setDoubleValue:current];
}

- (void)SetCurrentProgress:(double)current total:(double)total
{
[_progressCurrent setMaxValue:total];
[_progressCurrent setDoubleValue:current];
}

- (void)setRepresentedObject:(id)representedObject
{
[super setRepresentedObject:representedObject];
}

@end
@@ -0,0 +1,22 @@
// Copyright 2019 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <Cocoa/Cocoa.h>

int main(int argc, const char** argv)
{
if (argc == 1)
{
NSAlert* alert = [[[NSAlert alloc] init] autorelease];

[alert setMessageText:@"This updater is not meant to be launched directly."];
[alert setAlertStyle:NSAlertStyleWarning];
[alert setInformativeText:@"Configure Auto-Update in Dolphin's settings instead."];
[alert runModal];

return 1;
}

return NSApplicationMain(argc, argv);
}
@@ -19,23 +19,50 @@
#include <Windows.h>
#endif

#ifdef __APPLE__
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#endif

#if defined _WIN32 || defined __APPLE__
#define OS_SUPPORTS_UPDATER
#endif

namespace
{
#ifdef _WIN32

const char UPDATER_FILENAME[] = "Updater.exe";
const char UPDATER_RELOC_FILENAME[] = "Updater.2.exe";

#elif defined(__APPLE__)

const char UPDATER_FILENAME[] = "Dolphin Updater.app";
const char UPDATER_RELOC_FILENAME[] = ".Dolphin Updater.2.app";

#endif

const char UPDATER_LOG_FILE[] = "Updater.log";

std::wstring MakeUpdaterCommandLine(const std::map<std::string, std::string>& flags)
#ifdef OS_SUPPORTS_UPDATER
std::string MakeUpdaterCommandLine(const std::map<std::string, std::string>& flags)
{
std::wstring cmdline = UTF8ToUTF16(UPDATER_FILENAME) + L" "; // Start with a fake argv[0].
#ifdef __APPLE__
std::string cmdline = "\"" + File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME +
"/Contents/MacOS/Dolphin Updater\"";
#else
std::string cmdline = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME;
#endif

cmdline += " ";

for (const auto& pair : flags)
{
std::string value = "--" + pair.first + "=" + pair.second;
value = ReplaceAll(value, "\"", "\\\""); // Escape double quotes.
value = "\"" + value + "\" ";
cmdline += UTF8ToUTF16(value);
cmdline += value;
}
return cmdline;
}
@@ -44,7 +71,12 @@ std::wstring MakeUpdaterCommandLine(const std::map<std::string, std::string>& fl
void CleanupFromPreviousUpdate()
{
std::string reloc_updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME;

#ifdef __APPLE__
File::DeleteDirRecursively(reloc_updater_path);
#else
File::Delete(reloc_updater_path);
#endif
}
#endif

@@ -93,28 +125,40 @@ std::string GenerateChangelog(const picojson::array& versions)

bool AutoUpdateChecker::SystemSupportsAutoUpdates()
{
#ifdef _WIN32
#if defined _WIN32 || defined __APPLE__
return true;
#else
return false;
#endif
}

static std::string GetPlatformID()
{
#if defined _WIN32
return "win";
#elif defined __APPLE__
return "macos";
#else
return "unknown";
#endif
}

void AutoUpdateChecker::CheckForUpdate()
{
// Don't bother checking if updates are not supported or not enabled.
if (!SystemSupportsAutoUpdates() || SConfig::GetInstance().m_auto_update_track.empty())
return;

#ifdef _WIN32
#ifdef OS_SUPPORTS_UPDATER
CleanupFromPreviousUpdate();
#endif

std::string version_hash = SConfig::GetInstance().m_auto_update_hash_override.empty() ?
SCM_REV_STR :
SConfig::GetInstance().m_auto_update_hash_override;
std::string url = "https://dolphin-emu.org/update/check/v0/" +
SConfig::GetInstance().m_auto_update_track + "/" + version_hash;
std::string url = "https://dolphin-emu.org/update/check/v1/" +
SConfig::GetInstance().m_auto_update_track + "/" + version_hash + "/" +
GetPlatformID();

Common::HttpRequest req{std::chrono::seconds{10}};
auto resp = req.Get(url);
@@ -157,12 +201,16 @@ void AutoUpdateChecker::CheckForUpdate()
void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInformation& info,
AutoUpdateChecker::RestartMode restart_mode)
{
#ifdef _WIN32
#ifdef OS_SUPPORTS_UPDATER
std::map<std::string, std::string> updater_flags;
updater_flags["this-manifest-url"] = info.this_manifest_url;
updater_flags["next-manifest-url"] = info.next_manifest_url;
updater_flags["content-store-url"] = info.content_store_url;
#ifdef _WIN32
updater_flags["parent-pid"] = std::to_string(GetCurrentProcessId());
#else
updater_flags["parent-pid"] = std::to_string(getpid());
#endif
updater_flags["install-base-path"] = File::GetExeDirectory();
updater_flags["log-file"] = File::GetExeDirectory() + DIR_SEP + UPDATER_LOG_FILE;

@@ -172,19 +220,35 @@ void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInforma
// Copy the updater so it can update itself if needed.
std::string updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_FILENAME;
std::string reloc_updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME;

#ifdef __APPLE__
File::CopyDir(updater_path, reloc_updater_path);
chmod((reloc_updater_path + "/Contents/MacOS/Dolphin Updater").c_str(), 0700);
#else
File::Copy(updater_path, reloc_updater_path);
#endif

// Run the updater!
std::wstring command_line = MakeUpdaterCommandLine(updater_flags);
std::string command_line = MakeUpdaterCommandLine(updater_flags);

INFO_LOG(COMMON, "Updater command line: %s", command_line.c_str());

#ifdef _WIN32
STARTUPINFO sinfo = {sizeof(info)};
sinfo.dwFlags = STARTF_FORCEOFFFEEDBACK; // No hourglass cursor after starting the process.
PROCESS_INFORMATION pinfo;
INFO_LOG(COMMON, "Updater command line: %s", UTF16ToUTF8(command_line).c_str());
if (!CreateProcessW(UTF8ToUTF16(reloc_updater_path).c_str(),
const_cast<wchar_t*>(command_line.c_str()), nullptr, nullptr, FALSE, 0,
nullptr, nullptr, &sinfo, &pinfo))
const_cast<wchar_t*>(UTF8ToUTF16(command_line).c_str()), nullptr, nullptr,
FALSE, 0, nullptr, nullptr, &sinfo, &pinfo))
{
ERROR_LOG(COMMON, "Could not start updater process: error=%d", GetLastError());
}
#else
if (popen(command_line.c_str(), "r") == nullptr)
{
ERROR_LOG(COMMON, "Could not start updater process: error=%d", errno);
}
#endif

#endif
}