Updater: Mac support

This commit is contained in:
Stenzek 2023-09-23 15:43:12 +10:00
parent a115b40ef7
commit 30fdffae03
13 changed files with 703 additions and 36 deletions

View File

@ -334,6 +334,22 @@ jobs:
if: steps.cache-deps-mac.outputs.cache-hit != 'true' if: steps.cache-deps-mac.outputs.cache-hit != 'true'
run: scripts/build-dependencies-mac.sh 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 - name: Compile and zip .app
shell: bash shell: bash
run: | run: |

View File

@ -173,7 +173,6 @@ Requirements (Debian/Ubuntu package names):
5. Run the binary, located in the build directory under `bin/duckstation-qt`. 5. Run the binary, located in the build directory under `bin/duckstation-qt`.
### macOS ### 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: Requirements:
- CMake - CMake

View File

@ -3,7 +3,7 @@ add_subdirectory(util)
add_subdirectory(core) add_subdirectory(core)
add_subdirectory(scmversion) add_subdirectory(scmversion)
if(WIN32) if(WIN32 OR APPLE)
add_subdirectory(updater) add_subdirectory(updater)
endif() endif()

View File

@ -11,6 +11,7 @@
#include "common/file_system.h" #include "common/file_system.h"
#include "common/log.h" #include "common/log.h"
#include "common/minizip_helpers.h" #include "common/minizip_helpers.h"
#include "common/path.h"
#include "common/string_util.h" #include "common/string_util.h"
#include <QtCore/QCoreApplication> #include <QtCore/QCoreApplication>
@ -28,18 +29,18 @@
#include <QtWidgets/QMessageBox> #include <QtWidgets/QMessageBox>
#include <QtWidgets/QProgressDialog> #include <QtWidgets/QProgressDialog>
Log_SetChannel(AutoUpdaterDialog); #ifdef __APPLE__
#include "common/cocoa_tools.h"
#endif
// Logic to detect whether we can use the auto updater. // 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. // Requires that the channel be defined by the buildbot.
#if defined(_WIN32) || defined(__linux__)
#if defined(__has_include) && __has_include("scmversion/tag.h") #if defined(__has_include) && __has_include("scmversion/tag.h")
#include "scmversion/tag.h" #include "scmversion/tag.h"
#ifdef SCM_RELEASE_TAGS #ifdef SCM_RELEASE_TAGS
#define AUTO_UPDATER_SUPPORTED #define AUTO_UPDATER_SUPPORTED
#endif #endif
#endif #endif
#endif
#ifdef AUTO_UPDATER_SUPPORTED #ifdef AUTO_UPDATER_SUPPORTED
@ -52,6 +53,8 @@ static const char* THIS_RELEASE_TAG = SCM_RELEASE_TAG;
#endif #endif
Log_SetChannel(AutoUpdaterDialog);
AutoUpdaterDialog::AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent /* = nullptr */) AutoUpdaterDialog::AutoUpdaterDialog(EmuThread* host_interface, QWidget* parent /* = nullptr */)
: QDialog(parent), m_host_interface(host_interface) : QDialog(parent), m_host_interface(host_interface)
{ {
@ -81,7 +84,7 @@ bool AutoUpdaterDialog::isSupported()
return true; return true;
#else #else
// Windows - always supported. // Windows/Mac - always supported.
return true; return true;
#endif #endif
#else #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__) #elif defined(__linux__)
bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data) bool AutoUpdaterDialog::processUpdate(const QByteArray& update_data)

View File

@ -14,3 +14,29 @@ if(WIN32)
target_link_libraries(updater PRIVATE "Comctl32.lib") target_link_libraries(updater PRIVATE "Comctl32.lib")
set_target_properties(updater PROPERTIES WIN32_EXECUTABLE TRUE) set_target_properties(updater PROPERTIES WIN32_EXECUTABLE TRUE)
endif() 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()

24
src/updater/Info.plist.in Normal file
View File

@ -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>

BIN
src/updater/Updater.icns Normal file

Binary file not shown.

135
src/updater/cocoa_main.mm Normal file
View File

@ -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;
}

View File

@ -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;
};

View File

@ -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];
}
}

View File

@ -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) // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "updater.h" #include "updater.h"
#include "win32_progress_callback.h"
#include "common/error.h"
#include "common/file_system.h" #include "common/file_system.h"
#include "common/log.h" #include "common/log.h"
#include "common/minizip_helpers.h" #include "common/minizip_helpers.h"
#include "common/path.h"
#include "common/progress_callback.h"
#include "common/string_util.h" #include "common/string_util.h"
#include <algorithm> #include <algorithm>
@ -18,7 +20,14 @@
#include <vector> #include <vector>
#ifdef _WIN32 #ifdef _WIN32
#include "common/windows_headers.h"
#include <shellapi.h> #include <shellapi.h>
#else
#include <sys/stat.h>
#endif
#ifdef __APPLE__
#include "common/cocoa_tools.h"
#endif #endif
Updater::Updater(ProgressCallback* progress) : m_progress(progress) Updater::Updater(ProgressCallback* progress) : m_progress(progress)
@ -32,19 +41,12 @@ Updater::~Updater()
unzClose(m_zf); 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_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("Destination directory: '%s'", m_destination_directory.c_str());
m_progress->DisplayFormattedInformation("Staging directory: '%s'", m_staging_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; return true;
} }
@ -58,9 +60,12 @@ bool Updater::OpenUpdateZip(const char* path)
return ParseZip(); return ParseZip();
} }
bool Updater::RecursiveDeleteDirectory(const char* path) bool Updater::RecursiveDeleteDirectory(const char* path, bool remove_dir)
{ {
#ifdef _WIN32 #ifdef _WIN32
if (!remove_dir)
return false;
// making this safer on Win32... // making this safer on Win32...
std::wstring wpath(StringUtil::UTF8StringToWideString(path)); std::wstring wpath(StringUtil::UTF8StringToWideString(path));
wpath += L'\0'; wpath += L'\0';
@ -72,7 +77,31 @@ bool Updater::RecursiveDeleteDirectory(const char* path)
return (SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted); return (SHFileOperationW(&op) == 0 && !op.fAnyOperationsAborted);
#else #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 #endif
} }
@ -110,13 +139,33 @@ bool Updater::ParseZip()
while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER) while (zip_filename_buffer[0] == FS_OSPATH_SEPARATOR_CHARACTER)
std::memmove(&zip_filename_buffer[1], &zip_filename_buffer[0], --len); 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) // skip directories (we sort them out later)
if (len > 0 && zip_filename_buffer[len - 1] != FS_OSPATH_SEPARATOR_CHARACTER) 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. // 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_progress->DisplayFormattedInformation("Found file in zip: '%s'", entry.destination_filename.c_str());
m_update_paths.push_back(std::move(entry)); m_update_paths.push_back(std::move(entry));
} }
@ -167,7 +216,7 @@ bool Updater::PrepareStagingDirectory()
if (FileSystem::DirectoryExists(m_staging_directory.c_str())) if (FileSystem::DirectoryExists(m_staging_directory.c_str()))
{ {
m_progress->DisplayFormattedWarning("Update staging directory already exists, removing"); 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())) FileSystem::DirectoryExists(m_staging_directory.c_str()))
{ {
m_progress->ModalError("Failed to remove old staging directory"); m_progress->ModalError("Failed to remove old staging directory");
@ -204,7 +253,8 @@ bool Updater::StageUpdate()
for (const FileToUpdate& ftu : m_update_paths) 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) 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); std::fclose(fp);
unzCloseCurrentFile(m_zf); unzCloseCurrentFile(m_zf);
m_progress->IncrementProgressValue(); m_progress->IncrementProgressValue();
@ -291,17 +358,23 @@ bool Updater::CommitUpdate()
const std::string dest_file_name = StringUtil::StdStringFromFormat( const std::string dest_file_name = StringUtil::StdStringFromFormat(
"%s" FS_OSPATH_SEPARATOR_STR "%s", m_destination_directory.c_str(), ftu.destination_filename.c_str()); "%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()); m_progress->DisplayFormattedInformation("Moving '%s' to '%s'", staging_file_name.c_str(), dest_file_name.c_str());
Error error;
#ifdef _WIN32 #ifdef _WIN32
const bool result = const bool result =
MoveFileExW(StringUtil::UTF8StringToWideString(staging_file_name).c_str(), MoveFileExW(StringUtil::UTF8StringToWideString(staging_file_name).c_str(),
StringUtil::UTF8StringToWideString(dest_file_name).c_str(), MOVEFILE_REPLACE_EXISTING); 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 #else
const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0); const bool result = (rename(staging_file_name.c_str(), dest_file_name.c_str()) == 0);
#endif #endif
if (!result) if (!result)
{ {
m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s'", staging_file_name.c_str(), m_progress->DisplayFormattedModalError("Failed to rename '%s' to '%s': %s", staging_file_name.c_str(),
dest_file_name.c_str()); dest_file_name.c_str(), error.GetDescription().c_str());
return false; return false;
} }
} }
@ -312,6 +385,11 @@ bool Updater::CommitUpdate()
void Updater::CleanupStagingDirectory() void Updater::CleanupStagingDirectory()
{ {
// remove staging directory itself // 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()); 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);
}

View File

@ -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) // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#pragma once #pragma once
@ -13,27 +13,29 @@ public:
Updater(ProgressCallback* progress); Updater(ProgressCallback* progress);
~Updater(); ~Updater();
bool Initialize(std::string destination_directory); bool Initialize(std::string staging_directory, std::string destination_directory);
bool OpenUpdateZip(const char* path); bool OpenUpdateZip(const char* path);
bool PrepareStagingDirectory(); bool PrepareStagingDirectory();
bool StageUpdate(); bool StageUpdate();
bool CommitUpdate(); bool CommitUpdate();
void CleanupStagingDirectory(); void CleanupStagingDirectory();
bool ClearDestinationDirectory();
private: private:
static bool RecursiveDeleteDirectory(const char* path); bool RecursiveDeleteDirectory(const char* path, bool remove_dir);
struct FileToUpdate struct FileToUpdate
{ {
std::string original_zip_filename; std::string original_zip_filename;
std::string destination_filename; std::string destination_filename;
u32 file_mode;
}; };
bool ParseZip(); bool ParseZip();
std::string m_destination_directory;
std::string m_staging_directory; std::string m_staging_directory;
std::string m_destination_directory;
std::vector<FileToUpdate> m_update_paths; std::vector<FileToUpdate> m_update_paths;
std::vector<std::string> m_update_directories; std::vector<std::string> m_update_directories;

View File

@ -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) // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
#include "updater.h" #include "updater.h"
@ -6,6 +6,7 @@
#include "common/file_system.h" #include "common/file_system.h"
#include "common/log.h" #include "common/log.h"
#include "common/path.h"
#include "common/string_util.h" #include "common/string_util.h"
#include "common/windows_headers.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 int parent_process_id = StringUtil::FromChars<int>(StringUtil::WideStringToUTF8String(argv[0])).value_or(0);
const std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]); std::string destination_directory = StringUtil::WideStringToUTF8String(argv[1]);
const std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]); std::string staging_directory = Path::Combine(destination_directory, "UPDATE_STAGING");
const std::wstring program_to_launch(argv[3]); std::string zip_path = StringUtil::WideStringToUTF8String(argv[2]);
std::wstring program_to_launch(argv[3]);
LocalFree(argv); LocalFree(argv);
if (parent_process_id <= 0 || destination_directory.empty() || zip_path.empty() || program_to_launch.empty()) 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; 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); progress.SetFormattedStatusText("Waiting for parent process %d to exit...", parent_process_id);
WaitForProcessToExit(parent_process_id); WaitForProcessToExit(parent_process_id);
Updater updater(&progress); 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."); progress.ModalError("Failed to initialize updater.");
return 1; return 1;