mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-10 08:09:26 +01:00
Merge pull request #7717 from spycrab/macos_updater
Implement MacOS Updater
This commit is contained in:
commit
46918f420d
@ -587,6 +587,11 @@ else()
|
|||||||
set(PNG png)
|
set(PNG png)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
message(STATUS "Using ed25519 from Externals")
|
||||||
|
add_subdirectory(Externals/ed25519)
|
||||||
|
endif()
|
||||||
|
|
||||||
# Using static soundtouch from Externals
|
# Using static soundtouch from Externals
|
||||||
# Unable to use system soundtouch library: We require shorts, not floats.
|
# Unable to use system soundtouch library: We require shorts, not floats.
|
||||||
add_subdirectory(Externals/soundtouch)
|
add_subdirectory(Externals/soundtouch)
|
||||||
|
13
Externals/ed25519/CMakeLists.txt
vendored
Normal file
13
Externals/ed25519/CMakeLists.txt
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
project(ed25519 C)
|
||||||
|
|
||||||
|
add_library(ed25519
|
||||||
|
add_scalar.c
|
||||||
|
ge.c
|
||||||
|
keypair.c
|
||||||
|
seed.c
|
||||||
|
sign.c
|
||||||
|
fe.c
|
||||||
|
key_exchange.c
|
||||||
|
sc.c
|
||||||
|
sha512.c
|
||||||
|
verify.c)
|
@ -11,3 +11,7 @@ add_subdirectory(VideoBackends)
|
|||||||
if(ENABLE_QT)
|
if(ENABLE_QT)
|
||||||
add_subdirectory(DolphinQt)
|
add_subdirectory(DolphinQt)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (APPLE)
|
||||||
|
add_subdirectory(MacUpdater)
|
||||||
|
endif()
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
#include <CoreFoundation/CFBundle.h>
|
#include <CoreFoundation/CFBundle.h>
|
||||||
#include <CoreFoundation/CFString.h>
|
#include <CoreFoundation/CFString.h>
|
||||||
#include <CoreFoundation/CFURL.h>
|
#include <CoreFoundation/CFURL.h>
|
||||||
|
#include <mach-o/dyld.h>
|
||||||
#include <sys/param.h>
|
#include <sys/param.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -683,6 +684,9 @@ std::string GetExePath()
|
|||||||
dolphin_path = TStrToUTF8(dolphin_exe_expanded_path);
|
dolphin_path = TStrToUTF8(dolphin_exe_expanded_path);
|
||||||
else
|
else
|
||||||
dolphin_path = TStrToUTF8(dolphin_exe_path);
|
dolphin_path = TStrToUTF8(dolphin_exe_path);
|
||||||
|
#elif defined(__APPLE__)
|
||||||
|
dolphin_path = GetBundleDirectory();
|
||||||
|
dolphin_path = dolphin_path.substr(0, dolphin_path.find_last_of("Dolphin.app/Contents/MacOS"));
|
||||||
#else
|
#else
|
||||||
char dolphin_exe_path[PATH_MAX];
|
char dolphin_exe_path[PATH_MAX];
|
||||||
ssize_t len = ::readlink("/proc/self/exe", dolphin_exe_path, sizeof(dolphin_exe_path));
|
ssize_t len = ::readlink("/proc/self/exe", dolphin_exe_path, sizeof(dolphin_exe_path));
|
||||||
|
9
Source/Core/MacUpdater/AppDelegate.h
Normal file
9
Source/Core/MacUpdater/AppDelegate.h
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright 2019 Dolphin Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||||
|
|
||||||
|
@end
|
794
Source/Core/MacUpdater/AppDelegate.mm
Normal file
794
Source/Core/MacUpdater/AppDelegate.mm
Normal file
@ -0,0 +1,794 @@
|
|||||||
|
// Copyright 2019 Dolphin Emulator Project
|
||||||
|
// Licensed under GPLv2+
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
|
||||||
|
#include "UICommon/AutoUpdate.h"
|
||||||
|
|
||||||
|
#include <Cocoa/Cocoa.h>
|
||||||
|
#include <OptionParser.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <ed25519/ed25519.h>
|
||||||
|
#include <mbedtls/base64.h>
|
||||||
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <optional>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <thread>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <zlib.h>
|
||||||
|
|
||||||
|
#include "Common/CommonPaths.h"
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Common/FileUtil.h"
|
||||||
|
#include "Common/HttpRequest.h"
|
||||||
|
#include "Common/StringUtil.h"
|
||||||
|
|
||||||
|
#include "MacUpdater/UI.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// Public key used to verify update manifests.
|
||||||
|
const u8 UPDATE_PUB_KEY[] = {0x2a, 0xb3, 0xd1, 0xdc, 0x6e, 0xf5, 0x07, 0xf6, 0xa0, 0x6c, 0x7c,
|
||||||
|
0x54, 0xdf, 0x54, 0xf4, 0x42, 0x80, 0xa6, 0x28, 0x8b, 0x6d, 0x70,
|
||||||
|
0x14, 0xb5, 0x4c, 0x34, 0x95, 0x20, 0x4d, 0xd4, 0xd3, 0x5d};
|
||||||
|
|
||||||
|
const char UPDATE_TEMP_DIR[] = "TempUpdate";
|
||||||
|
|
||||||
|
// Where to log updater output.
|
||||||
|
FILE* log_fp = stderr;
|
||||||
|
|
||||||
|
void FlushLog()
|
||||||
|
{
|
||||||
|
fflush(log_fp);
|
||||||
|
fclose(log_fp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal representation of options passed on the command-line.
|
||||||
|
struct Options
|
||||||
|
{
|
||||||
|
std::string this_manifest_url;
|
||||||
|
std::string next_manifest_url;
|
||||||
|
std::string content_store_url;
|
||||||
|
std::string install_base_path;
|
||||||
|
std::optional<std::string> binary_to_restart;
|
||||||
|
std::optional<pid_t> parent_pid;
|
||||||
|
std::optional<std::string> log_file;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::optional<Options> ParseCommandLine(std::vector<std::string>& args)
|
||||||
|
{
|
||||||
|
using optparse::OptionParser;
|
||||||
|
|
||||||
|
OptionParser parser =
|
||||||
|
OptionParser().prog("Dolphin Updater").description("Dolphin Updater binary");
|
||||||
|
|
||||||
|
parser.add_option("--this-manifest-url")
|
||||||
|
.dest("this-manifest-url")
|
||||||
|
.help("URL to the update manifest for the currently installed version.")
|
||||||
|
.metavar("URL");
|
||||||
|
parser.add_option("--next-manifest-url")
|
||||||
|
.dest("next-manifest-url")
|
||||||
|
.help("URL to the update manifest for the to-be-installed version.")
|
||||||
|
.metavar("URL");
|
||||||
|
parser.add_option("--content-store-url")
|
||||||
|
.dest("content-store-url")
|
||||||
|
.help("Base URL of the content store where files to download are stored.")
|
||||||
|
.metavar("URL");
|
||||||
|
parser.add_option("--install-base-path")
|
||||||
|
.dest("install-base-path")
|
||||||
|
.help("Base path of the Dolphin install to be updated.")
|
||||||
|
.metavar("PATH");
|
||||||
|
parser.add_option("--binary-to-restart")
|
||||||
|
.dest("binary-to-restart")
|
||||||
|
.help("Binary to restart after the update is over.")
|
||||||
|
.metavar("PATH");
|
||||||
|
parser.add_option("--log-file")
|
||||||
|
.dest("log-file")
|
||||||
|
.help("File where to log updater debug output.")
|
||||||
|
.metavar("PATH");
|
||||||
|
parser.add_option("--parent-pid")
|
||||||
|
.dest("parent-pid")
|
||||||
|
.type("int")
|
||||||
|
.help("(optional) PID of the parent process. The updater will wait for this process to "
|
||||||
|
"complete before proceeding.")
|
||||||
|
.metavar("PID");
|
||||||
|
|
||||||
|
optparse::Values options = parser.parse_args(args);
|
||||||
|
|
||||||
|
Options opts;
|
||||||
|
|
||||||
|
// Required arguments.
|
||||||
|
std::vector<std::string> required{"this-manifest-url", "next-manifest-url", "content-store-url",
|
||||||
|
"install-base-path"};
|
||||||
|
for (const auto& req : required)
|
||||||
|
{
|
||||||
|
if (!options.is_set(req))
|
||||||
|
{
|
||||||
|
parser.print_help();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts.this_manifest_url = options["this-manifest-url"];
|
||||||
|
opts.next_manifest_url = options["next-manifest-url"];
|
||||||
|
opts.content_store_url = options["content-store-url"];
|
||||||
|
opts.install_base_path = options["install-base-path"];
|
||||||
|
|
||||||
|
// Optional arguments.
|
||||||
|
if (options.is_set("binary-to-restart"))
|
||||||
|
opts.binary_to_restart = options["binary-to-restart"];
|
||||||
|
if (options.is_set("parent-pid"))
|
||||||
|
opts.parent_pid = (pid_t)options.get("parent-pid");
|
||||||
|
if (options.is_set("log-file"))
|
||||||
|
opts.log_file = options["log-file"];
|
||||||
|
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> GzipInflate(const std::string& data)
|
||||||
|
{
|
||||||
|
z_stream zstrm;
|
||||||
|
zstrm.zalloc = nullptr;
|
||||||
|
zstrm.zfree = nullptr;
|
||||||
|
zstrm.opaque = nullptr;
|
||||||
|
zstrm.avail_in = static_cast<u32>(data.size());
|
||||||
|
zstrm.next_in = reinterpret_cast<u8*>(const_cast<char*>(data.data()));
|
||||||
|
|
||||||
|
// 16 + MAX_WBITS means gzip. Don't ask me.
|
||||||
|
inflateInit2(&zstrm, 16 + MAX_WBITS);
|
||||||
|
|
||||||
|
std::string out;
|
||||||
|
char buffer[4096];
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
zstrm.avail_out = sizeof(buffer);
|
||||||
|
zstrm.next_out = reinterpret_cast<u8*>(buffer);
|
||||||
|
|
||||||
|
ret = inflate(&zstrm, 0);
|
||||||
|
out.append(buffer, sizeof(buffer) - zstrm.avail_out);
|
||||||
|
} while (ret == Z_OK);
|
||||||
|
|
||||||
|
inflateEnd(&zstrm);
|
||||||
|
|
||||||
|
if (ret != Z_STREAM_END)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not read the data as gzip: error %d.\n", ret);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool VerifySignature(const std::string& data, const std::string& b64_signature)
|
||||||
|
{
|
||||||
|
u8 signature[64]; // ed25519 sig size.
|
||||||
|
size_t sig_size;
|
||||||
|
|
||||||
|
if (mbedtls_base64_decode(signature, sizeof(signature), &sig_size,
|
||||||
|
reinterpret_cast<const u8*>(b64_signature.data()),
|
||||||
|
b64_signature.size()) ||
|
||||||
|
sig_size != sizeof(signature))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Invalid base64: %s\n", b64_signature.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ed25519_verify(signature, reinterpret_cast<const u8*>(data.data()), data.size(),
|
||||||
|
UPDATE_PUB_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Manifest
|
||||||
|
{
|
||||||
|
using Filename = std::string;
|
||||||
|
using Hash = std::array<u8, 16>;
|
||||||
|
std::map<Filename, Hash> entries;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool HexDecode(const std::string& hex, u8* buffer, size_t size)
|
||||||
|
{
|
||||||
|
if (hex.size() != size * 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
auto DecodeNibble = [](char c) -> std::optional<u8> {
|
||||||
|
if (c >= '0' && c <= '9')
|
||||||
|
return static_cast<u8>(c - '0');
|
||||||
|
else if (c >= 'a' && c <= 'f')
|
||||||
|
return static_cast<u8>(c - 'a' + 10);
|
||||||
|
else if (c >= 'A' && c <= 'F')
|
||||||
|
return static_cast<u8>(c - 'A' + 10);
|
||||||
|
else
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < size; ++i)
|
||||||
|
{
|
||||||
|
std::optional<u8> high = DecodeNibble(hex[2 * i]);
|
||||||
|
std::optional<u8> low = DecodeNibble(hex[2 * i + 1]);
|
||||||
|
|
||||||
|
if (!high || !low)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
buffer[i] = (*high << 4) | *low;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HexEncode(const u8* buffer, size_t size)
|
||||||
|
{
|
||||||
|
std::string out(size * 2, '\0');
|
||||||
|
|
||||||
|
for (size_t i = 0; i < size; ++i)
|
||||||
|
{
|
||||||
|
out[2 * i] = "0123456789abcdef"[buffer[i] >> 4];
|
||||||
|
out[2 * i + 1] = "0123456789abcdef"[buffer[i] & 0xF];
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<Manifest> ParseManifest(const std::string& manifest)
|
||||||
|
{
|
||||||
|
Manifest parsed;
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
while (pos < manifest.size())
|
||||||
|
{
|
||||||
|
size_t filename_end_pos = manifest.find('\t', pos);
|
||||||
|
if (filename_end_pos == std::string::npos)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Manifest entry %zu: could not find filename end.\n", parsed.entries.size());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
size_t hash_end_pos = manifest.find('\n', filename_end_pos);
|
||||||
|
if (hash_end_pos == std::string::npos)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Manifest entry %zu: could not find hash end.\n", parsed.entries.size());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string filename = manifest.substr(pos, filename_end_pos - pos);
|
||||||
|
std::string hash = manifest.substr(filename_end_pos + 1, hash_end_pos - filename_end_pos - 1);
|
||||||
|
if (hash.size() != 32)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(),
|
||||||
|
hash.c_str());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
Manifest::Hash decoded_hash;
|
||||||
|
if (!HexDecode(hash, decoded_hash.data(), decoded_hash.size()))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Manifest entry %zu: invalid hash: \"%s\".\n", parsed.entries.size(),
|
||||||
|
hash.c_str());
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.entries[filename] = decoded_hash;
|
||||||
|
pos = hash_end_pos + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not showing a progress bar here because this part is just too quick
|
||||||
|
std::optional<Manifest> FetchAndParseManifest(const std::string& url)
|
||||||
|
{
|
||||||
|
Common::HttpRequest http;
|
||||||
|
|
||||||
|
Common::HttpRequest::Response resp = http.Get(url);
|
||||||
|
if (!resp)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Manifest download failed.\n");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||||
|
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||||
|
if (!maybe_decompressed)
|
||||||
|
return {};
|
||||||
|
std::string decompressed = std::move(*maybe_decompressed);
|
||||||
|
|
||||||
|
// Split into manifest and signature.
|
||||||
|
size_t boundary = decompressed.rfind("\n\n");
|
||||||
|
if (boundary == std::string::npos)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "No signature was found in manifest.\n");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string signature_block = decompressed.substr(boundary + 2); // 2 for "\n\n".
|
||||||
|
decompressed.resize(boundary + 1); // 1 to keep the final "\n".
|
||||||
|
|
||||||
|
std::vector<std::string> signatures = SplitString(signature_block, '\n');
|
||||||
|
bool found_valid_signature = false;
|
||||||
|
for (const auto& signature : signatures)
|
||||||
|
{
|
||||||
|
if (VerifySignature(decompressed, signature))
|
||||||
|
{
|
||||||
|
found_valid_signature = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found_valid_signature)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not verify signature of the manifest.\n");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ParseManifest(decompressed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represent the operations to be performed by the updater.
|
||||||
|
struct TodoList
|
||||||
|
{
|
||||||
|
struct DownloadOp
|
||||||
|
{
|
||||||
|
Manifest::Filename filename;
|
||||||
|
Manifest::Hash hash;
|
||||||
|
};
|
||||||
|
std::vector<DownloadOp> to_download;
|
||||||
|
|
||||||
|
struct UpdateOp
|
||||||
|
{
|
||||||
|
Manifest::Filename filename;
|
||||||
|
std::optional<Manifest::Hash> old_hash;
|
||||||
|
Manifest::Hash new_hash;
|
||||||
|
};
|
||||||
|
std::vector<UpdateOp> to_update;
|
||||||
|
|
||||||
|
struct DeleteOp
|
||||||
|
{
|
||||||
|
Manifest::Filename filename;
|
||||||
|
Manifest::Hash old_hash;
|
||||||
|
};
|
||||||
|
std::vector<DeleteOp> to_delete;
|
||||||
|
|
||||||
|
void Log() const
|
||||||
|
{
|
||||||
|
if (to_update.size())
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Updating:\n");
|
||||||
|
for (const auto& op : to_update)
|
||||||
|
{
|
||||||
|
std::string old_desc =
|
||||||
|
op.old_hash ? HexEncode(op.old_hash->data(), op.old_hash->size()) : "(new)";
|
||||||
|
fprintf(log_fp, " - %s: %s -> %s\n", op.filename.c_str(), old_desc.c_str(),
|
||||||
|
HexEncode(op.new_hash.data(), op.new_hash.size()).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (to_delete.size())
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Deleting:\n");
|
||||||
|
for (const auto& op : to_delete)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, " - %s (%s)\n", op.filename.c_str(),
|
||||||
|
HexEncode(op.old_hash.data(), op.old_hash.size()).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
TodoList ComputeActionsToDo(Manifest this_manifest, Manifest next_manifest)
|
||||||
|
{
|
||||||
|
TodoList todo;
|
||||||
|
|
||||||
|
// Delete if present in this manifest but not in next manifest.
|
||||||
|
for (const auto& entry : this_manifest.entries)
|
||||||
|
{
|
||||||
|
if (next_manifest.entries.find(entry.first) == next_manifest.entries.end())
|
||||||
|
{
|
||||||
|
TodoList::DeleteOp del;
|
||||||
|
del.filename = entry.first;
|
||||||
|
del.old_hash = entry.second;
|
||||||
|
todo.to_delete.push_back(std::move(del));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download and update if present in next manifest with different hash from this manifest.
|
||||||
|
for (const auto& entry : next_manifest.entries)
|
||||||
|
{
|
||||||
|
std::optional<Manifest::Hash> old_hash;
|
||||||
|
|
||||||
|
const auto& old_entry = this_manifest.entries.find(entry.first);
|
||||||
|
if (old_entry != this_manifest.entries.end())
|
||||||
|
old_hash = old_entry->second;
|
||||||
|
|
||||||
|
if (!old_hash || *old_hash != entry.second)
|
||||||
|
{
|
||||||
|
TodoList::DownloadOp download;
|
||||||
|
download.filename = entry.first;
|
||||||
|
download.hash = entry.second;
|
||||||
|
|
||||||
|
todo.to_download.push_back(std::move(download));
|
||||||
|
|
||||||
|
TodoList::UpdateOp update;
|
||||||
|
update.filename = entry.first;
|
||||||
|
update.old_hash = old_hash;
|
||||||
|
update.new_hash = entry.second;
|
||||||
|
todo.to_update.push_back(std::move(update));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return todo;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> FindOrCreateTempDir(const std::string& base_path)
|
||||||
|
{
|
||||||
|
std::string temp_path = base_path + DIR_SEP + UPDATE_TEMP_DIR;
|
||||||
|
int counter = 0;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if (!File::Exists(temp_path))
|
||||||
|
{
|
||||||
|
if (File::CreateDir(temp_path))
|
||||||
|
{
|
||||||
|
return temp_path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Couldn't create temp directory.\n");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (File::IsDirectory(temp_path))
|
||||||
|
{
|
||||||
|
return temp_path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Try again with a counter appended to the path.
|
||||||
|
std::string suffix = UPDATE_TEMP_DIR + std::to_string(counter);
|
||||||
|
temp_path = base_path + DIR_SEP + suffix;
|
||||||
|
}
|
||||||
|
} while (counter++ < 10);
|
||||||
|
|
||||||
|
fprintf(log_fp, "Could not find an appropriate temp directory name. Giving up.\n");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CleanUpTempDir(const std::string& temp_dir, const TodoList& todo)
|
||||||
|
{
|
||||||
|
// This is best-effort cleanup, we ignore most errors.
|
||||||
|
for (const auto& download : todo.to_download)
|
||||||
|
File::Delete(temp_dir + DIR_SEP + HexEncode(download.hash.data(), download.hash.size()));
|
||||||
|
File::DeleteDir(temp_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Manifest::Hash ComputeHash(const std::string& contents)
|
||||||
|
{
|
||||||
|
std::array<u8, 32> full;
|
||||||
|
mbedtls_sha256(reinterpret_cast<const u8*>(contents.data()), contents.size(), full.data(), false);
|
||||||
|
|
||||||
|
Manifest::Hash out;
|
||||||
|
std::copy(full.begin(), full.begin() + 16, out.begin());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProgressCallback(double total, double now, double, double)
|
||||||
|
{
|
||||||
|
UI::SetCurrentProgress(static_cast<int>(now), static_cast<int>(total));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DownloadContent(const std::vector<TodoList::DownloadOp>& to_download,
|
||||||
|
const std::string& content_base_url, const std::string& temp_path)
|
||||||
|
{
|
||||||
|
Common::HttpRequest req(std::chrono::seconds(30), ProgressCallback);
|
||||||
|
|
||||||
|
UI::SetTotalMarquee(false);
|
||||||
|
|
||||||
|
for (size_t i = 0; i < to_download.size(); i++)
|
||||||
|
{
|
||||||
|
UI::SetTotalProgress(static_cast<int>(i + 1), static_cast<int>(to_download.size()));
|
||||||
|
|
||||||
|
auto& download = to_download[i];
|
||||||
|
|
||||||
|
std::string hash_filename = HexEncode(download.hash.data(), download.hash.size());
|
||||||
|
UI::SetDescription("Downloading " + download.filename + "... (File " + std::to_string(i + 1) +
|
||||||
|
" of " + std::to_string(to_download.size()) + ")");
|
||||||
|
UI::SetCurrentMarquee(false);
|
||||||
|
|
||||||
|
// Add slashes where needed.
|
||||||
|
std::string content_store_path = hash_filename;
|
||||||
|
content_store_path.insert(4, "/");
|
||||||
|
content_store_path.insert(2, "/");
|
||||||
|
|
||||||
|
std::string url = content_base_url + content_store_path;
|
||||||
|
fprintf(log_fp, "Downloading %s ...\n", url.c_str());
|
||||||
|
|
||||||
|
auto resp = req.Get(url);
|
||||||
|
if (!resp)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
UI::SetCurrentMarquee(true);
|
||||||
|
UI::SetDescription("Verifying " + download.filename + "...");
|
||||||
|
|
||||||
|
std::string contents(reinterpret_cast<char*>(resp->data()), resp->size());
|
||||||
|
std::optional<std::string> maybe_decompressed = GzipInflate(contents);
|
||||||
|
if (!maybe_decompressed)
|
||||||
|
return false;
|
||||||
|
std::string decompressed = std::move(*maybe_decompressed);
|
||||||
|
|
||||||
|
// Check that the downloaded contents have the right hash.
|
||||||
|
Manifest::Hash contents_hash = ComputeHash(decompressed);
|
||||||
|
if (contents_hash != download.hash)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Wrong hash on downloaded content %s.\n", url.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string out = temp_path + DIR_SEP + hash_filename;
|
||||||
|
if (!File::WriteStringToFile(decompressed, out))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not write cache file %s.\n", out.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BackupFile(const std::string& path)
|
||||||
|
{
|
||||||
|
std::string backup_path = path + ".bak";
|
||||||
|
fprintf(log_fp, "Backing up unknown pre-existing %s to .bak.\n", path.c_str());
|
||||||
|
if (!File::Rename(path, backup_path))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Cound not rename %s to %s for backup.\n", path.c_str(), backup_path.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UpdateFiles(const std::vector<TodoList::UpdateOp>& to_update,
|
||||||
|
const std::string& install_base_path, const std::string& temp_path)
|
||||||
|
{
|
||||||
|
for (const auto& op : to_update)
|
||||||
|
{
|
||||||
|
std::string path = install_base_path + DIR_SEP + op.filename;
|
||||||
|
if (!File::CreateFullPath(path))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not create directory structure for %s.\n", op.filename.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (File::Exists(path))
|
||||||
|
{
|
||||||
|
std::string contents;
|
||||||
|
if (!File::ReadFileToString(path, contents))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not read existing file %s.\n", op.filename.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Manifest::Hash contents_hash = ComputeHash(contents);
|
||||||
|
if (contents_hash == op.new_hash)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "File %s was already up to date. Partial update?\n", op.filename.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (!op.old_hash || contents_hash != *op.old_hash)
|
||||||
|
{
|
||||||
|
if (!BackupFile(path))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we can safely move the new contents to the location.
|
||||||
|
std::string content_filename = HexEncode(op.new_hash.data(), op.new_hash.size());
|
||||||
|
fprintf(log_fp, "Updating file %s from content %s...\n", op.filename.c_str(),
|
||||||
|
content_filename.c_str());
|
||||||
|
if (!File::Copy(temp_path + DIR_SEP + content_filename, path))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not update file %s.\n", op.filename.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DeleteObsoleteFiles(const std::vector<TodoList::DeleteOp>& to_delete,
|
||||||
|
const std::string& install_base_path)
|
||||||
|
{
|
||||||
|
for (const auto& op : to_delete)
|
||||||
|
{
|
||||||
|
std::string path = install_base_path + DIR_SEP + op.filename;
|
||||||
|
|
||||||
|
if (!File::Exists(path))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "File %s is already missing.\n", op.filename.c_str());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::string contents;
|
||||||
|
if (!File::ReadFileToString(path, contents))
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Could not read file planned for deletion: %s.\n", op.filename.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Manifest::Hash contents_hash = ComputeHash(contents);
|
||||||
|
if (contents_hash != op.old_hash)
|
||||||
|
{
|
||||||
|
if (!BackupFile(path))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File::Delete(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PerformUpdate(const TodoList& todo, const std::string& install_base_path,
|
||||||
|
const std::string& content_base_url, const std::string& temp_path)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "Starting download step...\n");
|
||||||
|
if (!DownloadContent(todo.to_download, content_base_url, temp_path))
|
||||||
|
return false;
|
||||||
|
fprintf(log_fp, "Download step completed.\n");
|
||||||
|
|
||||||
|
fprintf(log_fp, "Starting update step...\n");
|
||||||
|
if (!UpdateFiles(todo.to_update, install_base_path, temp_path))
|
||||||
|
return false;
|
||||||
|
fprintf(log_fp, "Update step completed.\n");
|
||||||
|
|
||||||
|
fprintf(log_fp, "Starting deletion step...\n");
|
||||||
|
if (!DeleteObsoleteFiles(todo.to_delete, install_base_path))
|
||||||
|
return false;
|
||||||
|
fprintf(log_fp, "Deletion step completed.\n");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FatalError(const std::string& message)
|
||||||
|
{
|
||||||
|
fprintf(log_fp, "%s\n", message.c_str());
|
||||||
|
|
||||||
|
UI::Error(message);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
@interface AppDelegate ()
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification
|
||||||
|
{
|
||||||
|
UI::SetVisible(false);
|
||||||
|
|
||||||
|
NSArray* arguments = [[NSProcessInfo processInfo] arguments];
|
||||||
|
|
||||||
|
__block std::vector<std::string> args;
|
||||||
|
[arguments
|
||||||
|
enumerateObjectsUsingBlock:^(NSString* _Nonnull obj, NSUInteger idx, BOOL* _Nonnull stop) {
|
||||||
|
args.push_back(std::string([obj UTF8String]));
|
||||||
|
}];
|
||||||
|
|
||||||
|
std::optional<Options> maybe_opts = ParseCommandLine(args);
|
||||||
|
|
||||||
|
if (!maybe_opts)
|
||||||
|
{
|
||||||
|
[NSApp terminate:nil];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Options opts = std::move(*maybe_opts);
|
||||||
|
|
||||||
|
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
|
||||||
|
dispatch_async(queue, ^{
|
||||||
|
if (opts.log_file)
|
||||||
|
{
|
||||||
|
log_fp = fopen(opts.log_file.value().c_str(), "w");
|
||||||
|
if (!log_fp)
|
||||||
|
log_fp = stderr;
|
||||||
|
else
|
||||||
|
atexit(FlushLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(log_fp, "Updating from: %s\n", opts.this_manifest_url.c_str());
|
||||||
|
fprintf(log_fp, "Updating to: %s\n", opts.next_manifest_url.c_str());
|
||||||
|
fprintf(log_fp, "Install path: %s\n", opts.install_base_path.c_str());
|
||||||
|
|
||||||
|
if (!File::IsDirectory(opts.install_base_path))
|
||||||
|
{
|
||||||
|
FatalError("Cannot find install base path, or not a directory.");
|
||||||
|
|
||||||
|
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.parent_pid)
|
||||||
|
{
|
||||||
|
UI::SetDescription("Waiting for Dolphin to quit...");
|
||||||
|
|
||||||
|
fprintf(log_fp, "Waiting for parent PID %d to complete...\n", *opts.parent_pid);
|
||||||
|
|
||||||
|
auto pid = opts.parent_pid.value();
|
||||||
|
|
||||||
|
for (int res = kill(pid, 0); res == 0 || (res < 0 && errno == EPERM); res = kill(pid, 0))
|
||||||
|
{
|
||||||
|
sleep(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(log_fp, "Completed! Proceeding with update.\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
UI::SetVisible(true);
|
||||||
|
|
||||||
|
UI::SetDescription("Fetching and parsing manifests...");
|
||||||
|
|
||||||
|
Manifest this_manifest, next_manifest;
|
||||||
|
{
|
||||||
|
std::optional<Manifest> maybe_manifest = FetchAndParseManifest(opts.this_manifest_url);
|
||||||
|
if (!maybe_manifest)
|
||||||
|
{
|
||||||
|
FatalError("Could not fetch current manifest. Aborting.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this_manifest = std::move(*maybe_manifest);
|
||||||
|
|
||||||
|
maybe_manifest = FetchAndParseManifest(opts.next_manifest_url);
|
||||||
|
if (!maybe_manifest)
|
||||||
|
{
|
||||||
|
FatalError("Could not fetch next manifest. Aborting.");
|
||||||
|
[NSApp performSelector:@selector(terminate:) withObject:nil afterDelay:0.0];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
next_manifest = std::move(*maybe_manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
UI::SetDescription("Computing what to do...");
|
||||||
|
|
||||||
|
TodoList todo = ComputeActionsToDo(this_manifest, next_manifest);
|
||||||
|
todo.Log();
|
||||||
|
|
||||||
|
std::optional<std::string> maybe_temp_dir = FindOrCreateTempDir(opts.install_base_path);
|
||||||
|
if (!maybe_temp_dir)
|
||||||
|
return;
|
||||||
|
std::string temp_dir = std::move(*maybe_temp_dir);
|
||||||
|
|
||||||
|
UI::SetDescription("Performing Update...");
|
||||||
|
|
||||||
|
bool ok = PerformUpdate(todo, opts.install_base_path, opts.content_store_url, temp_dir);
|
||||||
|
if (!ok)
|
||||||
|
FatalError("Failed to apply the update.");
|
||||||
|
|
||||||
|
CleanUpTempDir(temp_dir, todo);
|
||||||
|
|
||||||
|
UI::ResetCurrentProgress();
|
||||||
|
UI::ResetTotalProgress();
|
||||||
|
UI::SetCurrentMarquee(false);
|
||||||
|
UI::SetTotalMarquee(false);
|
||||||
|
UI::SetCurrentProgress(1, 1);
|
||||||
|
UI::SetTotalProgress(1, 1);
|
||||||
|
UI::SetDescription("Done!");
|
||||||
|
|
||||||
|
// Let the user process that we are done.
|
||||||
|
[NSThread sleepForTimeInterval:1.0f];
|
||||||
|
|
||||||
|
if (opts.binary_to_restart)
|
||||||
|
{
|
||||||
|
[[NSWorkspace sharedWorkspace]
|
||||||
|
launchApplication:[NSString stringWithCString:opts.binary_to_restart.value().c_str()
|
||||||
|
encoding:[NSString defaultCStringEncoding]]];
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
|
[NSApp terminate:nil];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applicationWillTerminate:(NSNotification*)aNotification
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
63
Source/Core/MacUpdater/CMakeLists.txt
Normal file
63
Source/Core/MacUpdater/CMakeLists.txt
Normal file
@ -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()
|
||||||
|
|
32
Source/Core/MacUpdater/Info.plist.in
Normal file
32
Source/Core/MacUpdater/Info.plist.in
Normal file
@ -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>
|
109
Source/Core/MacUpdater/Main.storyboard
Normal file
109
Source/Core/MacUpdater/Main.storyboard
Normal file
@ -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>
|
24
Source/Core/MacUpdater/UI.h
Normal file
24
Source/Core/MacUpdater/UI.h
Normal file
@ -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
|
107
Source/Core/MacUpdater/UI.mm
Normal file
107
Source/Core/MacUpdater/UI.mm
Normal file
@ -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]; });
|
||||||
|
}
|
21
Source/Core/MacUpdater/ViewController.h
Normal file
21
Source/Core/MacUpdater/ViewController.h
Normal file
@ -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
|
52
Source/Core/MacUpdater/ViewController.m
Normal file
52
Source/Core/MacUpdater/ViewController.m
Normal file
@ -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
|
22
Source/Core/MacUpdater/main.m
Normal file
22
Source/Core/MacUpdater/main.m
Normal file
@ -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>
|
#include <Windows.h>
|
||||||
#endif
|
#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
|
namespace
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
|
||||||
const char UPDATER_FILENAME[] = "Updater.exe";
|
const char UPDATER_FILENAME[] = "Updater.exe";
|
||||||
const char UPDATER_RELOC_FILENAME[] = "Updater.2.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";
|
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)
|
for (const auto& pair : flags)
|
||||||
{
|
{
|
||||||
std::string value = "--" + pair.first + "=" + pair.second;
|
std::string value = "--" + pair.first + "=" + pair.second;
|
||||||
value = ReplaceAll(value, "\"", "\\\""); // Escape double quotes.
|
value = ReplaceAll(value, "\"", "\\\""); // Escape double quotes.
|
||||||
value = "\"" + value + "\" ";
|
value = "\"" + value + "\" ";
|
||||||
cmdline += UTF8ToUTF16(value);
|
cmdline += value;
|
||||||
}
|
}
|
||||||
return cmdline;
|
return cmdline;
|
||||||
}
|
}
|
||||||
@ -44,7 +71,12 @@ std::wstring MakeUpdaterCommandLine(const std::map<std::string, std::string>& fl
|
|||||||
void CleanupFromPreviousUpdate()
|
void CleanupFromPreviousUpdate()
|
||||||
{
|
{
|
||||||
std::string reloc_updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_FILENAME;
|
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);
|
File::Delete(reloc_updater_path);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -93,28 +125,40 @@ std::string GenerateChangelog(const picojson::array& versions)
|
|||||||
|
|
||||||
bool AutoUpdateChecker::SystemSupportsAutoUpdates()
|
bool AutoUpdateChecker::SystemSupportsAutoUpdates()
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#if defined _WIN32 || defined __APPLE__
|
||||||
return true;
|
return true;
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::string GetPlatformID()
|
||||||
|
{
|
||||||
|
#if defined _WIN32
|
||||||
|
return "win";
|
||||||
|
#elif defined __APPLE__
|
||||||
|
return "macos";
|
||||||
|
#else
|
||||||
|
return "unknown";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
void AutoUpdateChecker::CheckForUpdate()
|
void AutoUpdateChecker::CheckForUpdate()
|
||||||
{
|
{
|
||||||
// Don't bother checking if updates are not supported or not enabled.
|
// Don't bother checking if updates are not supported or not enabled.
|
||||||
if (!SystemSupportsAutoUpdates() || SConfig::GetInstance().m_auto_update_track.empty())
|
if (!SystemSupportsAutoUpdates() || SConfig::GetInstance().m_auto_update_track.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef OS_SUPPORTS_UPDATER
|
||||||
CleanupFromPreviousUpdate();
|
CleanupFromPreviousUpdate();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
std::string version_hash = SConfig::GetInstance().m_auto_update_hash_override.empty() ?
|
std::string version_hash = SConfig::GetInstance().m_auto_update_hash_override.empty() ?
|
||||||
SCM_REV_STR :
|
SCM_REV_STR :
|
||||||
SConfig::GetInstance().m_auto_update_hash_override;
|
SConfig::GetInstance().m_auto_update_hash_override;
|
||||||
std::string url = "https://dolphin-emu.org/update/check/v0/" +
|
std::string url = "https://dolphin-emu.org/update/check/v1/" +
|
||||||
SConfig::GetInstance().m_auto_update_track + "/" + version_hash;
|
SConfig::GetInstance().m_auto_update_track + "/" + version_hash + "/" +
|
||||||
|
GetPlatformID();
|
||||||
|
|
||||||
Common::HttpRequest req{std::chrono::seconds{10}};
|
Common::HttpRequest req{std::chrono::seconds{10}};
|
||||||
auto resp = req.Get(url);
|
auto resp = req.Get(url);
|
||||||
@ -157,12 +201,16 @@ void AutoUpdateChecker::CheckForUpdate()
|
|||||||
void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInformation& info,
|
void AutoUpdateChecker::TriggerUpdate(const AutoUpdateChecker::NewVersionInformation& info,
|
||||||
AutoUpdateChecker::RestartMode restart_mode)
|
AutoUpdateChecker::RestartMode restart_mode)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef OS_SUPPORTS_UPDATER
|
||||||
std::map<std::string, std::string> updater_flags;
|
std::map<std::string, std::string> updater_flags;
|
||||||
updater_flags["this-manifest-url"] = info.this_manifest_url;
|
updater_flags["this-manifest-url"] = info.this_manifest_url;
|
||||||
updater_flags["next-manifest-url"] = info.next_manifest_url;
|
updater_flags["next-manifest-url"] = info.next_manifest_url;
|
||||||
updater_flags["content-store-url"] = info.content_store_url;
|
updater_flags["content-store-url"] = info.content_store_url;
|
||||||
|
#ifdef _WIN32
|
||||||
updater_flags["parent-pid"] = std::to_string(GetCurrentProcessId());
|
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["install-base-path"] = File::GetExeDirectory();
|
||||||
updater_flags["log-file"] = File::GetExeDirectory() + DIR_SEP + UPDATER_LOG_FILE;
|
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.
|
// Copy the updater so it can update itself if needed.
|
||||||
std::string updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_FILENAME;
|
std::string updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_FILENAME;
|
||||||
std::string reloc_updater_path = File::GetExeDirectory() + DIR_SEP + UPDATER_RELOC_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);
|
File::Copy(updater_path, reloc_updater_path);
|
||||||
|
#endif
|
||||||
|
|
||||||
// Run the updater!
|
// 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)};
|
STARTUPINFO sinfo = {sizeof(info)};
|
||||||
sinfo.dwFlags = STARTF_FORCEOFFFEEDBACK; // No hourglass cursor after starting the process.
|
sinfo.dwFlags = STARTF_FORCEOFFFEEDBACK; // No hourglass cursor after starting the process.
|
||||||
PROCESS_INFORMATION pinfo;
|
PROCESS_INFORMATION pinfo;
|
||||||
INFO_LOG(COMMON, "Updater command line: %s", UTF16ToUTF8(command_line).c_str());
|
|
||||||
if (!CreateProcessW(UTF8ToUTF16(reloc_updater_path).c_str(),
|
if (!CreateProcessW(UTF8ToUTF16(reloc_updater_path).c_str(),
|
||||||
const_cast<wchar_t*>(command_line.c_str()), nullptr, nullptr, FALSE, 0,
|
const_cast<wchar_t*>(UTF8ToUTF16(command_line).c_str()), nullptr, nullptr,
|
||||||
nullptr, nullptr, &sinfo, &pinfo))
|
FALSE, 0, nullptr, nullptr, &sinfo, &pinfo))
|
||||||
{
|
{
|
||||||
ERROR_LOG(COMMON, "Could not start updater process: error=%d", GetLastError());
|
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
|
#endif
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user