// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <[email protected]> // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "cocoa_progress_callback.h" #include "updater.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 <cstdlib> #include <thread> static void LaunchApplication(const char* path) { @autoreleasepool { NSTask* task = [[[NSTask alloc] init] autorelease]; [task setLaunchPath:[NSString stringWithUTF8String:path]]; [task launch]; } } 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 != 4) { progress.ModalError("Expected 3 arguments: 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; } std::string zip_path = argv[1]; std::string staging_directory = argv[2]; std::string destination_directory = argv[3]; if (zip_path.empty() || staging_directory.empty() || destination_directory.empty()) { progress.ModalError("One or more parameters is empty."); 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 program_to_launch = Path::Combine(destination_directory, "Contents/MacOS/DuckStation"); int result = EXIT_SUCCESS; std::thread worker([&progress, 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]; }); }); 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 '{}'...", program_to_launch); LaunchApplication(program_to_launch.c_str()); } return result; }