Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
stenzek
GitHub Repository: stenzek/duckstation
Path: blob/master/src/updater/cocoa_main.mm
4243 views
// 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;
}