260 lines
7.3 KiB
C++
260 lines
7.3 KiB
C++
// Copyright 2018 Dolphin Emulator Project
|
|
// Licensed under GPLv2+
|
|
// Refer to the license.txt file included.
|
|
|
|
#include <windows.h>
|
|
#include <ShlObj.h>
|
|
|
|
#include <OptionParser.h>
|
|
#include <array>
|
|
#include <optional>
|
|
#include <shellapi.h>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
#include "Common/CommonPaths.h"
|
|
#include "Common/CommonTypes.h"
|
|
#include "Common/FileUtil.h"
|
|
|
|
#include "UpdaterCommon/UI.h"
|
|
#include "UpdaterCommon/UpdaterCommon.h"
|
|
|
|
namespace
|
|
{
|
|
// 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<DWORD> parent_pid;
|
|
std::optional<std::string> log_file;
|
|
};
|
|
|
|
std::vector<std::string> CommandLineToUtf8Argv(PCWSTR command_line)
|
|
{
|
|
int nargs;
|
|
LPWSTR* tokenized = CommandLineToArgvW(command_line, &nargs);
|
|
if (!tokenized)
|
|
return {};
|
|
|
|
std::vector<std::string> argv(nargs);
|
|
for (int i = 0; i < nargs; ++i)
|
|
{
|
|
argv[i] = UTF16ToUTF8(tokenized[i]);
|
|
}
|
|
|
|
LocalFree(tokenized);
|
|
return argv;
|
|
}
|
|
|
|
std::optional<Options> ParseCommandLine(PCWSTR command_line)
|
|
{
|
|
using optparse::OptionParser;
|
|
|
|
OptionParser parser = OptionParser().prog("updater.exe").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");
|
|
|
|
std::vector<std::string> argv = CommandLineToUtf8Argv(command_line);
|
|
optparse::Values options = parser.parse_args(argv);
|
|
|
|
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 = (DWORD)options.get("parent-pid");
|
|
if (options.is_set("log-file"))
|
|
opts.log_file = options["log-file"];
|
|
|
|
return opts;
|
|
}
|
|
}; // namespace
|
|
|
|
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow)
|
|
{
|
|
if (lstrlenW(pCmdLine) == 0)
|
|
{
|
|
MessageBox(nullptr,
|
|
L"This updater is not meant to be launched directly. Configure Auto-Update in "
|
|
"Dolphin's settings instead.",
|
|
L"Error", MB_ICONERROR);
|
|
return 1;
|
|
}
|
|
|
|
std::optional<Options> maybe_opts = ParseCommandLine(pCmdLine);
|
|
if (!maybe_opts)
|
|
return 1;
|
|
Options opts = std::move(*maybe_opts);
|
|
|
|
bool need_admin = false;
|
|
|
|
if (opts.log_file)
|
|
{
|
|
log_fp = _wfopen(UTF8ToUTF16(*opts.log_file).c_str(), L"w");
|
|
if (!log_fp)
|
|
{
|
|
log_fp = stderr;
|
|
// Failing to create the logfile for writing is a good indicator that we need administrator
|
|
// priviliges
|
|
need_admin = true;
|
|
}
|
|
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.");
|
|
return 1;
|
|
}
|
|
|
|
if (opts.parent_pid)
|
|
{
|
|
fprintf(log_fp, "Waiting for parent PID %d to complete...\n", *opts.parent_pid);
|
|
HANDLE parent_handle = OpenProcess(SYNCHRONIZE, FALSE, *opts.parent_pid);
|
|
WaitForSingleObject(parent_handle, INFINITE);
|
|
CloseHandle(parent_handle);
|
|
fprintf(log_fp, "Completed! Proceeding with update.\n");
|
|
}
|
|
|
|
if (need_admin)
|
|
{
|
|
if (IsUserAnAdmin())
|
|
{
|
|
FatalError("Failed to write to directory despite administrator priviliges.");
|
|
return 1;
|
|
}
|
|
|
|
wchar_t path[MAX_PATH];
|
|
if (GetModuleFileName(hInstance, path, sizeof(path)) == 0)
|
|
{
|
|
FatalError("Failed to get updater filename.");
|
|
return 1;
|
|
}
|
|
|
|
// Relaunch the updater as administrator
|
|
ShellExecuteW(nullptr, L"runas", path, pCmdLine, NULL, SW_SHOW);
|
|
return 0;
|
|
}
|
|
|
|
std::thread thread(UI::MessageLoop);
|
|
thread.detach();
|
|
|
|
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 1;
|
|
}
|
|
this_manifest = std::move(*maybe_manifest);
|
|
|
|
maybe_manifest = FetchAndParseManifest(opts.next_manifest_url);
|
|
if (!maybe_manifest)
|
|
{
|
|
FatalError("Could not fetch next manifest. Aborting.");
|
|
return 1;
|
|
}
|
|
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 1;
|
|
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(0, 1);
|
|
UI::SetTotalProgress(1, 1);
|
|
UI::SetDescription("Done!");
|
|
|
|
// Let the user process that we are done.
|
|
Sleep(1000);
|
|
|
|
if (opts.binary_to_restart)
|
|
{
|
|
// Hack: Launching the updater over the explorer ensures that admin priviliges are dropped. Why?
|
|
// Ask Microsoft.
|
|
ShellExecuteW(nullptr, nullptr, L"explorer.exe", UTF8ToUTF16(*opts.binary_to_restart).c_str(),
|
|
nullptr, SW_SHOW);
|
|
}
|
|
|
|
UI::Stop();
|
|
|
|
return !ok;
|
|
}
|