mirror of https://github.com/stenzek/duckstation
Updater: Mac support
parent
a115b40ef7
commit
30fdffae03
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Updater</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Updater.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.github.stenzek.duckstation.updater</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>English</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Licensed under GPL version 3</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>CSResourcesFileMapped</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
@ -0,0 +1,135 @@
|
||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR 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);
|
||||
|
||||
Log::SetConsoleOutputParams(true, "", LOGLEVEL_DEBUG);
|
||||
|
||||
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]; }); });
|
||||
|
||||
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.DisplayFormattedModalError("Could not open update zip '%s'. Update not installed.", zip_path.c_str());
|
||||
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();
|
||||
|
||||
progress.ModalInformation("Update complete.");
|
||||
|
||||
result = EXIT_SUCCESS;
|
||||
});
|
||||
|
||||
[NSApp run];
|
||||
|
||||
worker.join();
|
||||
|
||||
if (result == EXIT_SUCCESS)
|
||||
{
|
||||
progress.DisplayFormattedInformation("Launching '%s'...", program_to_launch.c_str());
|
||||
LaunchApplication(program_to_launch.c_str());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/progress_callback.h"
|
||||
|
||||
#include <AppKit/AppKit.h>
|
||||
#include <Cocoa/Cocoa.h>
|
||||
|
||||
#ifndef __OBJC__
|
||||
#error This file needs to be compiled with Objective C++.
|
||||
#endif
|
||||
|
||||
#if __has_feature(objc_arc)
|
||||
#error ARC should not be enabled.
|
||||
#endif
|
||||
|
||||
class CocoaProgressCallback final : public BaseProgressCallback
|
||||
{
|
||||
public:
|
||||
CocoaProgressCallback();
|
||||
~CocoaProgressCallback();
|
||||
|
||||
void PushState() override;
|
||||
void PopState() override;
|
||||
|
||||
void SetCancellable(bool cancellable) override;
|
||||
void SetTitle(const char* title) override;
|
||||
void SetStatusText(const char* text) override;
|
||||
void SetProgressRange(u32 range) override;
|
||||
void SetProgressValue(u32 value) override;
|
||||
|
||||
void DisplayError(const char* message) override;
|
||||
void DisplayWarning(const char* message) override;
|
||||
void DisplayInformation(const char* message) override;
|
||||
void DisplayDebugMessage(const char* message) override;
|
||||
|
||||
void ModalError(const char* message) override;
|
||||
bool ModalConfirmation(const char* message) override;
|
||||
void ModalInformation(const char* message) override;
|
||||
|
||||
private:
|
||||
enum : int
|
||||
{
|
||||
WINDOW_WIDTH = 600,
|
||||
WINDOW_HEIGHT = 300,
|
||||
WINDOW_MARGIN = 20,
|
||||
SUBWINDOW_PADDING = 10,
|
||||
SUBWINDOW_WIDTH = WINDOW_WIDTH - WINDOW_MARGIN - WINDOW_MARGIN,
|
||||
};
|
||||
|
||||
bool Create();
|
||||
void Destroy();
|
||||
void UpdateProgress();
|
||||
void AppendMessage(const char* message);
|
||||
|
||||
NSWindow* m_window = nil;
|
||||
NSView* m_view = nil;
|
||||
NSTextField* m_status = nil;
|
||||
NSProgressIndicator* m_progress = nil;
|
||||
NSScrollView* m_text_scroll = nil;
|
||||
NSTextView* m_text = nil;
|
||||
};
|
||||
@ -0,0 +1,244 @@
|
||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#include "cocoa_progress_callback.h"
|
||||
|
||||
#include "common/log.h"
|
||||
|
||||
Log_SetChannel(CocoaProgressCallback);
|
||||
|
||||
CocoaProgressCallback::CocoaProgressCallback() : BaseProgressCallback()
|
||||
{
|
||||
Create();
|
||||
}
|
||||
|
||||
CocoaProgressCallback::~CocoaProgressCallback()
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::PushState()
|
||||
{
|
||||
BaseProgressCallback::PushState();
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::PopState()
|
||||
{
|
||||
BaseProgressCallback::PopState();
|
||||
UpdateProgress();
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::SetCancellable(bool cancellable)
|
||||
{
|
||||
BaseProgressCallback::SetCancellable(cancellable);
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::SetTitle(const char* title)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), [this, title = [[NSString alloc] initWithUTF8String:title]]() {
|
||||
[m_window setTitle:title];
|
||||
[title release];
|
||||
});
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::SetStatusText(const char* text)
|
||||
{
|
||||
BaseProgressCallback::SetStatusText(text);
|
||||
dispatch_async(dispatch_get_main_queue(), [this, title = [[NSString alloc] initWithUTF8String:text]]() {
|
||||
[m_status setStringValue:title];
|
||||
[title release];
|
||||
});
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::SetProgressRange(u32 range)
|
||||
{
|
||||
BaseProgressCallback::SetProgressRange(range);
|
||||
UpdateProgress();
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::SetProgressValue(u32 value)
|
||||
{
|
||||
BaseProgressCallback::SetProgressValue(value);
|
||||
UpdateProgress();
|
||||
}
|
||||
|
||||
bool CocoaProgressCallback::Create()
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
const NSRect window_rect =
|
||||
NSMakeRect(0.0f, 0.0f, static_cast<float>(WINDOW_WIDTH), static_cast<float>(WINDOW_HEIGHT));
|
||||
constexpr NSWindowStyleMask style = NSWindowStyleMaskTitled;
|
||||
m_window = [[NSWindow alloc] initWithContentRect:window_rect
|
||||
styleMask:style
|
||||
backing:NSBackingStoreBuffered
|
||||
defer:NO];
|
||||
|
||||
NSView* m_view;
|
||||
m_view = [[NSView alloc] init];
|
||||
[m_window setContentView:m_view];
|
||||
|
||||
int x = WINDOW_MARGIN;
|
||||
int y = WINDOW_HEIGHT - WINDOW_MARGIN;
|
||||
|
||||
y -= 16 + SUBWINDOW_PADDING;
|
||||
m_status = [NSTextField labelWithString:@"Initializing..."];
|
||||
[m_status setFrame:NSMakeRect(x, y, SUBWINDOW_WIDTH, 16)];
|
||||
[m_view addSubview:m_status];
|
||||
|
||||
y -= 16 + SUBWINDOW_PADDING;
|
||||
m_progress = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(x, y, SUBWINDOW_WIDTH, 16)];
|
||||
[m_progress setMinValue:0];
|
||||
[m_progress setMaxValue:100];
|
||||
[m_progress setDoubleValue:0];
|
||||
[m_progress setIndeterminate:NO];
|
||||
[m_view addSubview:m_progress];
|
||||
|
||||
y -= 170 + SUBWINDOW_PADDING;
|
||||
m_text_scroll = [[NSScrollView alloc] initWithFrame:NSMakeRect(x, y, SUBWINDOW_WIDTH, 170)];
|
||||
[m_text_scroll setBorderType:NSBezelBorder];
|
||||
[m_text_scroll setHasVerticalScroller:YES];
|
||||
[m_text_scroll setHasHorizontalScroller:NO];
|
||||
|
||||
const NSSize content_size = [m_text_scroll contentSize];
|
||||
m_text = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, content_size.width, content_size.height)];
|
||||
[m_text setMinSize:NSMakeSize(0, content_size.height)];
|
||||
[m_text setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)];
|
||||
[m_text setVerticallyResizable:YES];
|
||||
[m_text setHorizontallyResizable:NO];
|
||||
[m_text setAutoresizingMask:NSViewWidthSizable];
|
||||
[m_text setUsesAdaptiveColorMappingForDarkAppearance:YES];
|
||||
[[m_text textContainer] setContainerSize:NSMakeSize(content_size.width, FLT_MAX)];
|
||||
[[m_text textContainer] setWidthTracksTextView:YES];
|
||||
[m_text_scroll setDocumentView:m_text];
|
||||
[m_view addSubview:m_text_scroll];
|
||||
|
||||
[m_window center];
|
||||
[m_window setIsVisible:TRUE];
|
||||
[m_window makeKeyAndOrderFront:nil];
|
||||
[m_window setReleasedWhenClosed:NO];
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::Destroy()
|
||||
{
|
||||
if (m_window == nil)
|
||||
return;
|
||||
|
||||
[m_window close];
|
||||
|
||||
m_text = nil;
|
||||
m_progress = nil;
|
||||
m_status = nil;
|
||||
|
||||
[m_view release];
|
||||
m_view = nil;
|
||||
|
||||
[m_window release];
|
||||
m_window = nil;
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::UpdateProgress()
|
||||
{
|
||||
const float percent = (static_cast<float>(m_progress_value) / static_cast<float>(m_progress_range)) * 100.0f;
|
||||
dispatch_async(dispatch_get_main_queue(), [this, percent]() {
|
||||
[m_progress setDoubleValue:percent];
|
||||
});
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::DisplayError(const char* message)
|
||||
{
|
||||
Log_ErrorPrint(message);
|
||||
AppendMessage(message);
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::DisplayWarning(const char* message)
|
||||
{
|
||||
Log_WarningPrint(message);
|
||||
AppendMessage(message);
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::DisplayInformation(const char* message)
|
||||
{
|
||||
Log_InfoPrint(message);
|
||||
AppendMessage(message);
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::AppendMessage(const char* message)
|
||||
{
|
||||
@autoreleasepool
|
||||
{
|
||||
NSString* nsmessage = [[[NSString stringWithUTF8String:message] stringByAppendingString:@"\n"] retain];
|
||||
dispatch_async(dispatch_get_main_queue(), [this, nsmessage]() {
|
||||
@autoreleasepool
|
||||
{
|
||||
NSAttributedString* attr = [[[NSAttributedString alloc] initWithString:nsmessage] autorelease];
|
||||
[[m_text textStorage] appendAttributedString:attr];
|
||||
[m_text scrollRangeToVisible:NSMakeRange([[m_text string] length], 0)];
|
||||
[nsmessage release];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::DisplayDebugMessage(const char* message)
|
||||
{
|
||||
Log_DevPrint(message);
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::ModalError(const char* message)
|
||||
{
|
||||
if (![NSThread isMainThread])
|
||||
{
|
||||
dispatch_sync(dispatch_get_main_queue(), [this, message]() { ModalError(message); });
|
||||
return;
|
||||
}
|
||||
|
||||
@autoreleasepool
|
||||
{
|
||||
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
|
||||
[alert setMessageText:[NSString stringWithUTF8String:message]];
|
||||
[alert setAlertStyle:NSAlertStyleCritical];
|
||||
[alert runModal];
|
||||
}
|
||||
}
|
||||
|
||||
bool CocoaProgressCallback::ModalConfirmation(const char* message)
|
||||
{
|
||||
if (![NSThread isMainThread])
|
||||
{
|
||||
bool result;
|
||||
dispatch_sync(dispatch_get_main_queue(), [this, message, &result]() { result = ModalConfirmation(message); });
|
||||
return result;
|
||||
}
|
||||
|
||||
bool result;
|
||||
@autoreleasepool
|
||||
{
|
||||
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
|
||||
[alert setMessageText:[NSString stringWithUTF8String:message]];
|
||||
[alert addButtonWithTitle:@"Yes"];
|
||||
[alert addButtonWithTitle:@"No"];
|
||||
result = ([alert runModal] == NSAlertFirstButtonReturn);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CocoaProgressCallback::ModalInformation(const char* message)
|
||||
{
|
||||
if (![NSThread isMainThread])
|
||||
{
|
||||
dispatch_sync(dispatch_get_main_queue(), [this, message]() { ModalInformation(message); });
|
||||
return;
|
||||
}
|
||||
|
||||
@autoreleasepool
|
||||
{
|
||||
NSAlert* alert = [[[NSAlert alloc] init] autorelease];
|
||||
[alert setMessageText:[NSString stringWithUTF8String:message]];
|
||||
[alert runModal];
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue