Skip to content

Commit

Permalink
Merge pull request #1528 from colincornaby/mac-self-patching
Browse files Browse the repository at this point in the history
Self patching Mac application
  • Loading branch information
colincornaby authored Jul 28, 2024
2 parents 44063ab + 7dc5c44 commit 2a60d41
Show file tree
Hide file tree
Showing 12 changed files with 324 additions and 23 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ find_package(ZLIB REQUIRED)
if(APPLE)
find_package(Security)
find_package(Metal)
find_package(LibArchive REQUIRED)
elseif(UNIX)
find_package(LIBSECRET)
find_package(Uuid REQUIRED)
Expand Down
1 change: 1 addition & 0 deletions Sources/Plasma/Apps/plClient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ target_link_libraries(
CURL::libcurl
"$<$<PLATFORM_ID:Darwin>:-framework Cocoa>"
"$<$<PLATFORM_ID:Darwin>:-framework QuartzCore>"
$<$<PLATFORM_ID:Darwin>:${LibArchive_LIBRARIES}>
)
target_include_directories(plClient PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="21701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment version="101200" identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21701"/>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/>
<capability name="System colors introduced in macOS 10.14" minToolsVersion="10.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
Expand All @@ -23,7 +23,7 @@
<windowStyleMask key="styleMask" titled="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="196" y="240" width="563" height="358"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<view key="contentView" id="se5-gp-TjO">
<rect key="frame" x="0.0" y="0.0" width="563" height="358"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
Expand Down Expand Up @@ -288,7 +288,7 @@ DQ
<windowStyleMask key="styleMask" titled="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
<rect key="contentRect" x="283" y="305" width="409" height="105"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<rect key="screenRect" x="0.0" y="0.0" width="1728" height="1079"/>
<view key="contentView" id="3bP-48-HfP">
<rect key="frame" x="0.0" y="0.0" width="409" height="105"/>
<autoresizingMask key="autoresizingMask"/>
Expand Down Expand Up @@ -335,6 +335,6 @@ Gw
</objects>
<resources>
<image name="Dirt" width="32" height="32"/>
<image name="banner" width="427" height="56"/>
<image name="banner" width="866" height="113.57746124267578"/>
</resources>
</document>
3 changes: 2 additions & 1 deletion Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ NS_ASSUME_NONNULL_BEGIN

- (void)patcher:(PLSPatcher*)patcher beganDownloadOfFile:(NSString*)file;
- (void)patcher:(PLSPatcher*)patcher updatedProgress:(NSString*)progressMessage withBytes:(NSUInteger)bytes outOf:(uint64_t)totalBytes;
- (void)patcherCompleted:(PLSPatcher*)patcher;
- (void)patcherCompleted:(PLSPatcher*)patcher didSelfPatch:(BOOL)selfPatched;
- (void)patcherCompletedWithError:(PLSPatcher*)patcher error:(NSError*)error;

@end
Expand All @@ -60,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN
@property(weak) id<PLSPatcherDelegate> delegate;
@property(readonly) BOOL selfPatched;

- (NSURL*)completeSelfPatch:(NSError **)error;
- (void)start;

@end
Expand Down
224 changes: 222 additions & 2 deletions Sources/Plasma/Apps/plClient/Mac-Cocoa/PLSPatcher.mm
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,20 @@
#import "PLSPatcher.h"
#import "NSString+StringTheory.h"

#include <archive.h>
#include <archive_entry.h>
#include <unordered_set>
#include <string_theory/format>

#include "HeadSpin.h"
#include "hsDarwin.h"
#include "hsTimer.h"

#include "pfPatcher/pfPatcher.h"
#include "pfPatcher/plManifests.h"
#include "plFileSystem.h"
#include "plNetGameLib/plNetGameLib.h"
#include "plStatusLog/plStatusLog.h"

class Patcher
{
Expand All @@ -61,13 +65,17 @@
void IOnPatchComplete(ENetError result, const ST::string& msg);
void IOnProgressTick(uint64_t curBytes, uint64_t totalBytes, const ST::string& status);
void IOnDownloadBegin(const plFileName& file);
void ISelfPatch(const plFileName& file);
plFileName IFindBundleExe(const plFileName& file);
};

@interface PLSPatcher ()
@property BOOL selfPatched;
@property pfPatcher* patcher;
@property NSTimer* networkPumpTimer;
@property Patcher cppPatcher;
@property NSURL* updatedClientURL;
@property NSURL* temporaryDirectory;
@end

@implementation PLSPatcher
Expand All @@ -88,6 +96,8 @@ - (id)init
_patcher->OnCompletion(std::bind(&Patcher::IOnPatchComplete, _cppPatcher, std::placeholders::_1,
std::placeholders::_2));
_patcher->OnFileDownloadDesired(IApproveDownload);
_patcher->OnSelfPatch(std::bind(&Patcher::ISelfPatch, _cppPatcher, std::placeholders::_1));
_patcher->OnFindBundleExe(std::bind(&Patcher::IFindBundleExe, _cppPatcher, std::placeholders::_1));

self.networkPumpTimer = [NSTimer timerWithTimeInterval:1.0 / 1000.0
repeats:true
Expand All @@ -106,6 +116,53 @@ - (void)start
self.patcher->Start();
}

- (NSURL *)completeSelfPatch:(NSError **)error;
{
NSString* destinationPath = [NSString stringWithSTString:plManifest::PatcherExecutable().AsString()];
NSURL* destinationURL = [NSURL fileURLWithPath:destinationPath];

NSError* errorInScope;

if (!self.updatedClientURL) {
// uh oh - this implies we weren't able to decompress the client
if (error) {
// Handle as a generic could not read file error.
// Bad compression on the server will require correction on the server end.
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileReadNoSuchFileError userInfo:nil];
}
return nil;
}

if ([NSFileManager.defaultManager fileExistsAtPath:destinationPath]) {
// need to swap
BOOL swapSucceeded = renamex_np(destinationURL.path.fileSystemRepresentation, self.updatedClientURL.path.fileSystemRepresentation, RENAME_SWAP) == 0;
if (swapSucceeded) {
// delete the old version - this is very likely us
// we want to terminate after. Our bundle will no longer be valid.
if (self.temporaryDirectory) {
[NSFileManager.defaultManager removeItemAtURL:self.temporaryDirectory error:&errorInScope];
}
} else {
// abort and return an error
errorInScope = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil];
}
} else {
// no executable already present! Just move things into place.
[NSFileManager.defaultManager moveItemAtURL:self.updatedClientURL toURL:destinationURL error:&errorInScope];
}

if (errorInScope) {
// Try to clean up if there was an error
[NSFileManager.defaultManager removeItemAtURL:self.updatedClientURL error:nil];
if (error) {
*error = errorInScope;
}
return nil;
}

return destinationURL;
}

void Patcher::IOnDownloadBegin(const plFileName& file)
{
NSString* fileName = [NSString stringWithSTString:file.AsString()];
Expand Down Expand Up @@ -139,27 +196,190 @@ bool IApproveDownload(const plFileName& file)
return extExcludeList.find(file.GetFileExt()) == extExcludeList.end();
}

static la_ssize_t copy_data(struct archive* ar, struct archive* aw)
{
while (true) {
la_ssize_t r;
const void* buff;
size_t size;
la_int64_t offset;

r = archive_read_data_block(ar, &buff, &size, &offset);
if (r == ARCHIVE_EOF)
return (ARCHIVE_OK);
if (r < ARCHIVE_OK)
return (r);
r = archive_write_data_block(aw, buff, size, offset);
if (r < ARCHIVE_OK) {
pfPatcher::GetLog()->AddLine(plStatusLog::kRed, archive_error_string(aw));
return (r);
}
}
}

void Patcher::ISelfPatch(const plFileName& file)
{
/*
Note on errors:
This function does not return errors, but a self patch
without a populated updatedClientURL will imply something
went wrong during decompress.
*/

PLSPatcher* patcher = parent;
patcher.selfPatched = true;

int flags;
la_ssize_t r;

/* Select which attributes we want to restore. */
flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM;

struct archive* a = archive_read_new();
struct archive* ext = archive_write_disk_new();

{
int error;
error = archive_read_support_format_tar(a);
hsAssert(error == ARCHIVE_OK, "Unable to set tar format option");
error = archive_read_support_filter_gzip(a);
hsAssert(error == ARCHIVE_OK, "Unable to set gzip filter");
error = archive_read_support_filter_bzip2(a);
hsAssert(error == ARCHIVE_OK, "Unable to set bzip filter");

error = archive_write_disk_set_options(ext, flags);
hsAssert(error == ARCHIVE_OK, "Unable to set write options");
error = archive_write_disk_set_standard_lookup(ext);
hsAssert(error == ARCHIVE_OK, "Unable to set write standard lookup");
}

if ((r = archive_read_open_filename(a, file.GetFileName().c_str(), 10240)) != ARCHIVE_OK) {
// couldn't read
archive_read_free(a);
archive_write_close(ext);
archive_write_free(ext);
return;
}

NSError* error;
NSURL* currentDirectory = [NSURL fileURLWithPath:NSFileManager.defaultManager.currentDirectoryPath];
patcher.temporaryDirectory = [NSFileManager.defaultManager
URLForDirectory:NSItemReplacementDirectory
inDomain:NSUserDomainMask
appropriateForURL:currentDirectory
create:YES error:&error];
NSURL* outputURL;
if (patcher.temporaryDirectory) {
outputURL = [patcher.temporaryDirectory URLByAppendingPathComponent:[NSString stringWithSTString:plManifest::PatcherExecutable().GetFileName()]];
[NSFileManager.defaultManager createDirectoryAtURL:outputURL withIntermediateDirectories:false attributes:nil error:&error];
}

if (error) {
// Not sure why things would go wrong, we should be able to
// get a writable temp directory. But if we could not, bail.
// Not populating the patched client path will be caught
// later.
archive_read_close(a);
archive_read_free(a);
archive_write_close(ext);
archive_write_free(ext);
return;
}

ST::string outputPath = [outputURL.path STString];

bool succeeded = true;

struct archive_entry* entry;
while (true) {
r = archive_read_next_header(a, &entry);
if (r == ARCHIVE_EOF)
break;
if (r < ARCHIVE_OK)
pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to read bundle archive: {}", archive_error_string(a));
if (r < ARCHIVE_WARN) {
succeeded = false;
break;
}
const char* currentFile = archive_entry_pathname(entry);
auto fullOutputPath = plFileName::Join(outputPath, currentFile);
archive_entry_set_pathname(entry, fullOutputPath.AsString().c_str());
r = archive_write_header(ext, entry);
if (r < ARCHIVE_OK) {
pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to extract file while patching app bundle: {}", archive_error_string(ext));
} else if (archive_entry_size(entry) > 0) {
r = copy_data(a, ext);
if (r < ARCHIVE_OK)
pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to extract file while patching app bundle: {}", archive_error_string(ext));
if (r < ARCHIVE_WARN) {
succeeded = false;
break;
}
}
r = archive_write_finish_entry(ext);
if (r < ARCHIVE_OK)
pfPatcher::GetLog()->AddLineF(plStatusLog::kRed, "Failed to extract file while patching app bundle: {}", archive_error_string(ext));
if (r < ARCHIVE_WARN) {
succeeded = false;
break;
}
}
archive_read_close(a);
archive_read_free(a);
archive_write_close(ext);
archive_write_free(ext);

plFileSystem::Unlink(file);

if (succeeded) {
parent.updatedClientURL = outputURL;
}
}

void Patcher::IOnPatchComplete(ENetError result, const ST::string& msg)
{
[parent.networkPumpTimer invalidate];
if (IS_NET_SUCCESS(result)) {
PLSPatcher* patcher = parent;
dispatch_async(dispatch_get_main_queue(), ^{
[patcher.delegate patcherCompleted:patcher];
[patcher.delegate patcherCompleted:patcher
didSelfPatch:patcher.selfPatched];
});
} else {
NSString* msgString = [NSString stringWithSTString:msg];

dispatch_async(dispatch_get_main_queue(), ^{
ST::string errorString = ST::string::from_wchar(NetErrorToString(result));
NSString* errorNSString = [NSString stringWithSTString:errorString];
[parent.delegate
patcherCompletedWithError:parent
error:[NSError errorWithDomain:@"PLSPatchErrors"
code:result
userInfo:@{
NSLocalizedFailureErrorKey : msgString
NSLocalizedFailureErrorKey : errorNSString,
NSLocalizedFailureReasonErrorKey: msgString
}]];
});
}
}

plFileName Patcher::IFindBundleExe(const plFileName& clientPath)
{
// If this is a Mac app bundle, MD5 the executable. The executable will hold the
// code signing hash - and thus unique the entire bundle.

@autoreleasepool {
NSURL* bundleURL = [NSURL fileURLWithPath:[NSString stringWithSTString:clientPath.AsString()]];
NSBundle* bundle = [NSBundle bundleWithURL:bundleURL];
NSURL* executableURL = [bundle executableURL];

if (executableURL) {
NSString* executablePath = [executableURL path];
return plFileName([[executableURL path] STString]);
}

return clientPath;
}
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ - (void)patcher:(PLSPatcher*)patcher
[NSString stringWithFormat:@"%@/%@", bytesString, totalBytesString];
}

- (void)patcherCompleted:(nonnull PLSPatcher*)patcher
- (void)patcherCompletedWithError:(nonnull PLSPatcher*)patcher error:(nonnull NSError*)error
{
// intercepted by the application
}

- (void)patcherCompletedWithError:(nonnull PLSPatcher*)patcher error:(nonnull NSError*)error
- (void)patcherCompleted:(nonnull PLSPatcher *)patcher didSelfPatch:(BOOL)selfPatched
{
// intercepted by the application
}
Expand Down
Loading

0 comments on commit 2a60d41

Please sign in to comment.