You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
duckstation/src/updater/cocoa_main.mm

213 lines
6.7 KiB
Plaintext

// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "cocoa_progress_callback.h"
#include "updater.h"
#include "common/cocoa_tools.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/path.h"
#include "common/scoped_guard.h"
#include "common/string_util.h"
#include "common/timer.h"
#include <cerrno>
#include <cstdlib>
#include <sys/event.h>
#include <sys/types.h>
#include <thread>
#include <unistd.h>
static bool WaitForProcessToExit(pid_t process_id, Error* error)
{
const int queue = kqueue();
if (queue < 0)
{
Error::SetErrno(error, "kqueue() failed: ", errno);
return false;
}
const ScopedGuard queue_closer = [queue]() { close(queue); };
struct kevent change;
EV_SET(&change, static_cast<uintptr_t>(process_id), EVFILT_PROC, EV_ADD | EV_ENABLE | EV_ONESHOT, NOTE_EXIT, 0,
nullptr);
struct kevent event;
int result;
do
{
result = kevent(queue, &change, 1, &event, 1, nullptr);
} while (result < 0 && errno == EINTR);
if (result < 0)
{
Error::SetErrno(error, "kevent() failed while waiting for DuckStation to exit: ", errno);
return false;
}
if (result == 0)
{
Error::SetString(error, "kevent() returned without reporting that DuckStation exited.");
return false;
}
if (event.flags & EV_ERROR)
{
// The process can exit between launching the updater and registering the event. In that case it is safe to
// continue. Any other registration error is fatal, because modifying the bundle while DuckStation is still
// running could leave the installation unusable.
const int event_error = static_cast<int>(event.data);
if (event_error == ESRCH)
return true;
Error::SetErrno(error, "Failed to monitor DuckStation: ", event_error);
return false;
}
return true;
}
int main(int argc, char* argv[])
{
[NSApplication sharedApplication];
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
// Needed for keyboard in put.
const ProcessSerialNumber psn = {0, kCurrentProcess};
TransformProcessType(&psn, kProcessTransformToForegroundApplication);
CocoaProgressCallback progress;
if (argc != 5)
{
progress.ModalError(
"Expected 4 arguments: parent process id, update zip, staging directory, output directory.\n\nThis program is "
"not intended to be run manually, please use the Qt frontend and click Help->Check for Updates.");
return EXIT_FAILURE;
}
const int parent_process_id = StringUtil::FromChars<int>(argv[1]).value_or(0);
std::string zip_path = argv[2];
std::string staging_directory = argv[3];
std::string destination_directory = argv[4];
if (parent_process_id <= 0 || zip_path.empty() || staging_directory.empty() || destination_directory.empty())
{
progress.ModalError("One or more parameters is invalid.");
return EXIT_FAILURE;
}
if (const char* home_dir = getenv("HOME"))
{
static constexpr char log_file[] = "Library/Application Support/DuckStation/updater.log";
std::string log_path = Path::Combine(home_dir, log_file);
Log::SetFileOutputParams(true, log_path.c_str());
}
std::string application_to_launch = destination_directory;
int result = EXIT_SUCCESS;
std::thread worker([&progress, parent_process_id, zip_path = std::move(zip_path),
destination_directory = std::move(destination_directory),
staging_directory = std::move(staging_directory), &result]() {
ScopedGuard app_stopper([]() {
dispatch_async(dispatch_get_main_queue(), []() {
[NSApp stop:nil];
// NSApp stop doesn't immediately exit the event loop, so we'll get stuck waiting until
// a key is pressed or the mouse is moved. Manually queue an event to ensure the run
// loop wakes and exits.
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
});
});
progress.FormatStatusText("Waiting for DuckStation process {} to exit...", parent_process_id);
Error wait_error;
if (!WaitForProcessToExit(static_cast<pid_t>(parent_process_id), &wait_error))
{
progress.FormatModalError("Failed to wait for DuckStation to exit: {}", wait_error.GetDescription());
result = EXIT_FAILURE;
return;
}
Updater updater(&progress);
if (!updater.Initialize(std::move(staging_directory), std::move(destination_directory)))
{
progress.ModalError("Failed to initialize updater.");
result = EXIT_FAILURE;
return;
}
if (!updater.OpenUpdateZip(zip_path.c_str()))
{
progress.FormatModalError("Could not open update zip '{}'. Update not installed.", zip_path);
result = EXIT_FAILURE;
return;
}
if (!updater.PrepareStagingDirectory())
{
progress.ModalError("Failed to prepare staging directory. Update not installed.");
result = EXIT_FAILURE;
return;
}
if (!updater.StageUpdate())
{
progress.ModalError("Failed to stage update. Update not installed.");
result = EXIT_FAILURE;
return;
}
if (!updater.ClearDestinationDirectory())
{
progress.ModalError("Failed to clear destination directory. Your installation may be corrupted, please "
"re-download a fresh version from GitHub.");
result = EXIT_FAILURE;
return;
}
if (!updater.CommitUpdate())
{
progress.ModalError(
"Failed to commit update. Your installation may be corrupted, please re-download a fresh version from GitHub.");
result = EXIT_FAILURE;
return;
}
updater.CleanupStagingDirectory();
updater.RemoveUpdateZip();
result = EXIT_SUCCESS;
});
[NSApp run];
worker.join();
if (result == EXIT_SUCCESS)
{
progress.FormatInformation("Launching '{}'...", application_to_launch);
Error launch_error;
if (!CocoaTools::LaunchApplication(application_to_launch, {}, &launch_error))
{
progress.FormatModalError("The update was installed successfully, but DuckStation could not be restarted: {}\n\n"
"Please launch DuckStation manually.",
launch_error.GetDescription());
result = EXIT_FAILURE;
}
}
return result;
}