Updater: Mac support
This commit is contained in:
parent
a115b40ef7
commit
30fdffae03
|
@ -334,6 +334,22 @@ jobs:
|
|||
if: steps.cache-deps-mac.outputs.cache-hit != 'true'
|
||||
run: scripts/build-dependencies-mac.sh
|
||||
|
||||
- name: Tag as preview build
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
echo '#pragma once' > src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_ASSET "duckstation-mac-release.zip"' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAG "preview"' >> src/scmversion/tag.h
|
||||
|
||||
- name: Tag as dev build
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: |
|
||||
echo '#pragma once' > src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_ASSET "duckstation-mac-release.zip"' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAG "latest"' >> src/scmversion/tag.h
|
||||
|
||||
- name: Compile and zip .app
|
||||
shell: bash
|
||||
run: |
|
||||
|
|
|
@ -173,7 +173,6 @@ Requirements (Debian/Ubuntu package names):
|
|||
5. Run the binary, located in the build directory under `bin/duckstation-qt`.
|
||||
|
||||
### macOS
|
||||
**NOTE:** macOS is highly experimental and not tested by the developer. Use at your own risk; things may be horribly broken. Vulkan support may be unstable, so sticking to OpenGL or software renderer is recommended.
|
||||
|
||||
Requirements:
|
||||
- CMake
|
||||
|
|
|
@ -3,7 +3,7 @@ add_subdirectory(util)
|
|||
add_subdirectory(core)
|
||||
add_subdirectory(scmversion)
|
||||
|
||||
if(WIN32)
|
||||
if(WIN32 OR APPLE)
|
||||
add_subdirectory(updater)
|
||||
endif()
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/minizip_helpers.h"
|
||||
#include "common/path.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
|
@ -28,18 +29,18 @@
|
|||
#include <QtWidgets/QMessageBox>
|
||||
#include <QtWidgets/QProgressDialog>
|
||||
|
||||
Log_SetChannel(AutoUpdaterDialog);
|
||||
#ifdef __APPLE__
|
||||
#include "common/cocoa_tools.h"
|
||||
#endif
|
||||
|
||||
// Logic to detect whether we can use the auto updater.
|
||||
// Currently Windows and Linux-only, and requires that the channel be defined by the buildbot.
|
||||
#if defined(_WIN32) || defined(__linux__)
|
||||
// Requires that the channel be defined by the buildbot.
|
||||
#if defined(__has_include) && __has_include("scmversion/tag.h")
|
||||
#include "scmversion/tag.h"
|
||||
#ifdef SCM_RELEASE_TAGS
|
||||
#define AUTO_UPDATER_SUPPORTED
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef AUTO_UPDATER_SUPPORTED
|
||||
|
||||
|
@ -52,6 +53,8 @@ static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG;
|
|||
|
||||
#endif
|
||||
|
||||
Log_SetChannel(AutoUpdaterDialog);
|
||||
|
||||
AutoUpdaterDialog::AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent /* = nullptr */)
|
||||
: QDialog(parent), m_host_interface(host_interface)
|
||||
{
|
||||
|
@ -81,7 +84,7 @@ bool AutoUpdaterDialog::isSupported()
|
|||
|
||||
return true;
|
||||
#else
|
||||
// Windows - always supported.
|
||||
// Windows/Mac - always supported.
|
||||
return true;
|
||||
#endif
|
||||
#else
|
||||
|
@ -582,6 +585,78 @@ void AutoUpdaterDialog::cleanupAfterUpdate()
|
|||
{
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
|
||||
{
|
||||
std::optional<std::string> bundle_path = CocoaTools::GetNonTranslocatedBundlePath();
|
||||
if (!bundle_path.has_value())
|
||||
{
|
||||
reportError("Couldn't obtain non-translocated bundle path.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QFileInfo info(QString::fromStdString(bundle_path.value()));
|
||||
if (!info.isBundle())
|
||||
{
|
||||
reportError("Application %s isn't a bundle.", bundle_path->c_str());
|
||||
return false;
|
||||
}
|
||||
if (info.suffix() != QStringLiteral("app"))
|
||||
{
|
||||
reportError("Unexpected application suffix %s on %s.", info.suffix().toUtf8().constData(), bundle_path->c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the updater from this version to unpack the new version.
|
||||
const std::string updater_app = Path::Combine(bundle_path.value(), "Contents/Resources/Updater.app");
|
||||
if (!FileSystem::DirectoryExists(updater_app.c_str()))
|
||||
{
|
||||
reportError("Failed to find updater at %s.", updater_app.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// We use the user data directory to temporarily store the update zip.
|
||||
const std::string zip_path = Path::Combine(EmuFolders::DataRoot, "update.zip");
|
||||
const std::string staging_directory = Path::Combine(EmuFolders::DataRoot, "UPDATE_STAGING");
|
||||
if (FileSystem::FileExists(zip_path.c_str()) && !FileSystem::DeleteFile(zip_path.c_str()))
|
||||
{
|
||||
reportError("Failed to remove old update zip.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save update.
|
||||
{
|
||||
QFile zip_file(QString::fromStdString(zip_path));
|
||||
if (!zip_file.open(QIODevice::WriteOnly) || zip_file.write(update_data) != update_data.size())
|
||||
{
|
||||
reportError("Writing update zip to '%s' failed", zip_path.c_str());
|
||||
return false;
|
||||
}
|
||||
zip_file.close();
|
||||
}
|
||||
|
||||
Log_InfoFmt("Beginning update:\nUpdater path: {}\nZip path: {}\nStaging directory: {}\nOutput directory: {}",
|
||||
updater_app, zip_path, staging_directory, bundle_path.value());
|
||||
|
||||
const std::string_view args[] = {
|
||||
zip_path,
|
||||
staging_directory,
|
||||
bundle_path.value(),
|
||||
};
|
||||
|
||||
// Kick off updater!
|
||||
CocoaTools::DelayedLaunch(updater_app, args);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AutoUpdaterDialog::cleanupAfterUpdate()
|
||||
{
|
||||
const QString zip_path = QString::fromStdString(Path::Combine(EmuFolders::DataRoot, "update.zip"));
|
||||
if (QFile::exists(zip_path))
|
||||
QFile::remove(zip_path);
|
||||
}
|
||||
|
||||
#elif defined(__linux__)
|
||||
|
||||
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)
|
||||
|
|
|
@ -14,3 +14,29 @@ if(WIN32)
|
|||
target_link_libraries(updater PRIVATE "Comctl32.lib")
|
||||
set_target_properties(updater PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(MAC_SOURCES
|
||||
cocoa_main.mm
|
||||
cocoa_progress_callback.mm
|
||||
cocoa_progress_callback.h
|
||||
)
|
||||
target_sources(updater PRIVATE ${MAC_SOURCES})
|
||||
set_source_files_properties(${MAC_SOURCES} PROPERTIES SKIP_PRECOMPILE_HEADERS TRUE)
|
||||
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
||||
target_link_libraries(updater PRIVATE ${COCOA_LIBRARY})
|
||||
|
||||
if(NOT CMAKE_GENERATOR MATCHES "Xcode" AND NOT SKIP_POSTPROCESS_BUNDLE)
|
||||
set_target_properties(updater PROPERTIES OUTPUT_NAME "Updater")
|
||||
set_target_properties(updater PROPERTIES
|
||||
MACOSX_BUNDLE true
|
||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in
|
||||
OUTPUT_NAME Updater
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/DuckStation.app/Contents/Resources
|
||||
)
|
||||
|
||||
# Copy icon into the bundle
|
||||
target_sources(updater PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/Updater.icns")
|
||||
set_source_files_properties("${CMAKE_CURRENT_SOURCE_DIR}/Updater.icns" PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#include "updater.h"
|
||||
#include "win32_progress_callback.h"
|
||||
|
||||
#include "common/error.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/minizip_helpers.h"
|
||||
#include "common/path.h"
|
||||
#include "common/progress_callback.h"
|
||||
#include "common/string_util.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
@ -18,7 +20,14 @@
|
|||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include "common/windows_headers.h"
|
||||
#include <shellapi.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#include "common/cocoa_tools.h"
|
||||
#endif
|
||||
|
||||
Updater::Updater(ProgressCallback* progress) : m_progress(progress)
|
||||
|
@ -32,19 +41,12 @@ Updater::~Updater()
|
|||
unzClose(m_zf);
|
||||
}
|
||||
|
||||
bool Updater::Initialize(std::string destination_directory)
|
||||
bool Updater::Initialize(std::string staging_directory, std::string destination_directory)
|
||||
{
|
||||
m_staging_directory = std::move(staging_directory);
|
||||
m_destination_directory = std::move(destination_directory);
|
||||
m_staging_directory = StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "%s",
|
||||
m_destination_directory.c_str(), "UPDATE_STAGING");
|
||||
m_progress->DisplayFormattedInformation("Destination directory: '%s'", m_destination_directory.c_str());
|
||||
m_progress->DisplayFormattedInformation("Staging directory: '%s'", m_staging_directory.c_str());
|
||||
|
||||
// log everything to file as well
|
||||
Log::SetFileOutputParams(
|
||||
true, StringUtil::StdStringFromFormat("%s" FS_OSPATH_SEPARATOR_STR "updater.log", m_destination_directory.c_str())
|
||||
.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -58,9 +60,12 @@ bool Updater::OpenUpdateZip(const char* path)
|
|||
return ParseZip();
|
||||
}
|
||||
|
||||
bool Updater::RecursiveDeleteDirectory(const char* path)
|
||||
bool Updater::RecursiveDeleteDirectory(const char* path, bool remove_dir)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!remove_dir)
|
||||
return false;
|
||||
|
||||
// making this safer on Win32...
|
||||
std::wstring wpath(StringUtil::UTF8StringToWideString(path));
|
||||
wpath += L'\0';
|
||||
|
@ -72,7 +77,31 @@ bool Updater::RecursiveDeleteDirectory(const char* path)
|
|||
|
||||
return (SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted);
|
||||
#else
|
||||
return FileSystem::DeleteDirectory(path, true);
|
||||
FileSystem::FindResultsArray results;
|
||||
if (FileSystem::FindFiles(path, "*", FILESYSTEM_FIND_FILES | FILESYSTEM_FIND_FOLDERS | FILESYSTEM_FIND_HIDDEN_FILES,
|
||||
&results))
|
||||
{
|
||||
for (const FILESYSTEM_FIND_DATA& fd : results)
|
||||
{
|
||||
if (fd.Attributes & FILESYSTEM_FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
if (!RecursiveDeleteDirectory(fd.FileName.c_str(), true))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_progress->DisplayFormattedInformation("Removing directory '%s'.", fd.FileName.c_str());
|
||||
if (!FileSystem::DeleteFile(fd.FileName.c_str()))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!remove_dir)
|
||||
return true;
|
||||
|
||||
m_progress->DisplayFormattedInformation("Removing directory '%s'.", path);
|
||||
return FileSystem::DeleteDirectory(path);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -110,13 +139,33 @@ bool Updater::ParseZip()
|
|||
while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER)
|
||||
std::memmove(&zip_filename_buffer[1], &zip_filename_buffer[0], --len);
|
||||
|
||||
#ifdef _WIN32
|
||||
entry.file_mode = 0;
|
||||
#else
|
||||
// Preserve permissions on Unix.
|
||||
static constexpr u32 PERMISSION_MASK = (S_IRWXO | S_IRWXG | S_IRWXU);
|
||||
entry.file_mode =
|
||||
((file_info.external_fa >> 16) & 0x01FFu) & PERMISSION_MASK; // https://stackoverflow.com/a/28753385
|
||||
#endif
|
||||
|
||||
// skip directories (we sort them out later)
|
||||
if (len > 0 && zip_filename_buffer[len - 1] != FS_OSPATH_SEPARATOR_CHARACTER)
|
||||
{
|
||||
bool process_file = true;
|
||||
const char* filename_to_add = zip_filename_buffer;
|
||||
#ifdef _WIN32
|
||||
// skip updater itself, since it was already pre-extracted.
|
||||
if (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0)
|
||||
process_file = process_file && (StringUtil::Strcasecmp(zip_filename_buffer, "updater.exe") != 0);
|
||||
#elif defined(__APPLE__)
|
||||
// on MacOS, we want to remove the DuckStation.app prefix.
|
||||
static constexpr const char* PREFIX_PATH = "DuckStation.app/";
|
||||
const size_t prefix_length = std::strlen(PREFIX_PATH);
|
||||
process_file = process_file && (std::strncmp(zip_filename_buffer, PREFIX_PATH, prefix_length) == 0);
|
||||
filename_to_add += prefix_length;
|
||||
#endif
|
||||
if (process_file)
|
||||
{
|
||||
entry.destination_filename = zip_filename_buffer;
|
||||
entry.destination_filename = filename_to_add;
|
||||
m_progress->DisplayFormattedInformation("Found file in zip: '%s'", entry.destination_filename.c_str());
|
||||
m_update_paths.push_back(std::move(entry));
|
||||
}
|
||||
|
@ -167,7 +216,7 @@ bool Updater::PrepareStagingDirectory()
|
|||
if (FileSystem::DirectoryExists(m_staging_directory.c_str()))
|
||||
{
|
||||
m_progress->DisplayFormattedWarning("Update staging directory already exists, removing");
|
||||
if (!RecursiveDeleteDirectory(m_staging_directory.c_str()) ||
|
||||
if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true) ||
|
||||
FileSystem::DirectoryExists(m_staging_directory.c_str()))
|
||||
{
|
||||
m_progress->ModalError("Failed to remove old staging directory");
|
||||
|
@ -204,7 +253,8 @@ bool Updater::StageUpdate()
|
|||
|
||||
for (const FileToUpdate& ftu : m_update_paths)
|
||||
{
|
||||
m_progress->SetFormattedStatusText("Extracting '%s'...", ftu.original_zip_filename.c_str());
|
||||
m_progress->SetFormattedStatusText("Extracting '%s' (mode %o)...", ftu.original_zip_filename.c_str(),
|
||||
ftu.file_mode);
|
||||
|
||||
if (unzLocateFile(m_zf, ftu.original_zip_filename.c_str(), 0) != UNZ_OK)
|
||||
{
|
||||
|
@ -258,6 +308,23 @@ bool Updater::StageUpdate()
|
|||
}
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
if (ftu.file_mode != 0)
|
||||
{
|
||||
const int fd = fileno(fp);
|
||||
const int res = (fd >= 0) ? fchmod(fd, ftu.file_mode) : -1;
|
||||
if (res < 0)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to set mode for file '%s' (fd %d) to %u: errno %d",
|
||||
destination_file.c_str(), fd, res, errno);
|
||||
std::fclose(fp);
|
||||
FileSystem::DeleteFile(destination_file.c_str());
|
||||
unzCloseCurrentFile(m_zf);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
std::fclose(fp);
|
||||
unzCloseCurrentFile(m_zf);
|
||||
m_progress->IncrementProgressValue();
|
||||
|
@ -291,17 +358,23 @@ bool Updater::CommitUpdate()
|
|||
const std::string dest_file_name = StringUtil::StdStringFromFormat(
|
||||
"%s" FS_OSPATH_SEPARATOR_STR "%s", m_destination_directory.c_str(), ftu.destination_filename.c_str());
|
||||
m_progress->DisplayFormattedInformation("Moving '%s' to '%s'", staging_file_name.c_str(), dest_file_name.c_str());
|
||||
|
||||
Error error;
|
||||
#ifdef _WIN32
|
||||
const bool result =
|
||||
MoveFileExW(StringUtil::UTF8StringToWideString(staging_file_name).c_str(),
|
||||
StringUtil::UTF8StringToWideString(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING);
|
||||
if (!result)
|
||||
error.SetWin32(GetLastError());
|
||||
#elif defined(__APPLE__)
|
||||
const bool result = CocoaTools::MoveFile(staging_file_name.c_str(), dest_file_name.c_str(), &error);
|
||||
#else
|
||||
const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0);
|
||||
#endif
|
||||
if (!result)
|
||||
{
|
||||
m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s'", staging_file_name.c_str(),
|
||||
dest_file_name.c_str());
|
||||
m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s': %s", staging_file_name.c_str(),
|
||||
dest_file_name.c_str(), error.GetDescription().c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -312,6 +385,11 @@ bool Updater::CommitUpdate()
|
|||
void Updater::CleanupStagingDirectory()
|
||||
{
|
||||
// remove staging directory itself
|
||||
if (!RecursiveDeleteDirectory(m_staging_directory.c_str()))
|
||||
if (!RecursiveDeleteDirectory(m_staging_directory.c_str(), true))
|
||||
m_progress->DisplayFormattedError("Failed to remove staging directory '%s'", m_staging_directory.c_str());
|
||||
}
|
||||
|
||||
bool Updater::ClearDestinationDirectory()
|
||||
{
|
||||
return RecursiveDeleteDirectory(m_destination_directory.c_str(), false);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#pragma once
|
||||
|
@ -13,27 +13,29 @@ public:
|
|||
Updater(ProgressCallback* progress);
|
||||
~Updater();
|
||||
|
||||
bool Initialize(std::string destination_directory);
|
||||
bool Initialize(std::string staging_directory, std::string destination_directory);
|
||||
|
||||
bool OpenUpdateZip(const char* path);
|
||||
bool PrepareStagingDirectory();
|
||||
bool StageUpdate();
|
||||
bool CommitUpdate();
|
||||
void CleanupStagingDirectory();
|
||||
bool ClearDestinationDirectory();
|
||||
|
||||
private:
|
||||
static bool RecursiveDeleteDirectory(const char* path);
|
||||
bool RecursiveDeleteDirectory(const char* path, bool remove_dir);
|
||||
|
||||
struct FileToUpdate
|
||||
{
|
||||
std::string original_zip_filename;
|
||||
std::string destination_filename;
|
||||
u32 file_mode;
|
||||
};
|
||||
|
||||
bool ParseZip();
|
||||
|
||||
std::string m_destination_directory;
|
||||
std::string m_staging_directory;
|
||||
std::string m_destination_directory;
|
||||
|
||||
std::vector<FileToUpdate> m_update_paths;
|
||||
std::vector<std::string> m_update_directories;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com>
|
||||
// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
|
||||
|
||||
#include "updater.h"
|
||||
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "common/path.h"
|
||||
#include "common/string_util.h"
|
||||
#include "common/windows_headers.h"
|
||||
|
||||
|
@ -42,9 +43,10 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi
|
|||
}
|
||||
|
||||
const int parent_process_id = StringUtil::FromChars<int>(StringUtil::WideStringToUTF8String(argv[0])).value_or(0);
|
||||
const std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]);
|
||||
const std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]);
|
||||
const std::wstring program_to_launch(argv[3]);
|
||||
std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]);
|
||||
std::string staging_directory = Path::Combine(destination_directory, "UPDATE_STAGING");
|
||||
std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]);
|
||||
std::wstring program_to_launch(argv[3]);
|
||||
LocalFree(argv);
|
||||
|
||||
if (parent_process_id <= 0 || destination_directory.empty() || zip_path.empty() || program_to_launch.empty())
|
||||
|
@ -53,11 +55,13 @@ int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLi
|
|||
return 1;
|
||||
}
|
||||
|
||||
Log::SetFileOutputParams(true, Path::Combine(destination_directory, "updater.log").c_str());
|
||||
|
||||
progress.SetFormattedStatusText("Waiting for parent process %d to exit...", parent_process_id);
|
||||
WaitForProcessToExit(parent_process_id);
|
||||
|
||||
Updater updater(&progress);
|
||||
if (!updater.Initialize(destination_directory))
|
||||
if (!updater.Initialize(std::move(staging_directory), std::move(destination_directory)))
|
||||
{
|
||||
progress.ModalError("Failed to initialize updater.");
|
||||
return 1;
|
||||
|
|
Loading…
Reference in New Issue