Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Self patching Mac application #1528

Merged
merged 32 commits into from
Jul 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8cedeec
Self patching Mac application
colincornaby Nov 19, 2023
be038c2
Feedback from code review
colincornaby Jan 9, 2024
eb84009
Externalizing Mac bundle executable finding
colincornaby Jan 27, 2024
0367fa7
Apply suggestions from code review
colincornaby Feb 4, 2024
76f6e52
Handling self patch copy errors
colincornaby Feb 4, 2024
dfb4750
Cleaning up exit calls in decompression function
colincornaby Feb 4, 2024
6afdf95
Apply suggestions from code review
colincornaby Feb 5, 2024
783df85
Constraining archive types to tar/gzip/bzip
colincornaby Feb 5, 2024
4a3cdac
Code cleanup in Mac client patching
colincornaby Feb 5, 2024
29cf9fd
Fixing renamex_np error handling
colincornaby Feb 5, 2024
418c28d
Apply suggestions from code review
colincornaby Feb 18, 2024
9593e8f
Adding feedback from code review
colincornaby Feb 18, 2024
0ac9d8d
Adding missing import
colincornaby Feb 18, 2024
2a216e2
Adding guard against temp directory create failure
colincornaby Feb 18, 2024
857f056
Moving variables into while loop
colincornaby Feb 18, 2024
ab84e40
Adding error handling for libarchive options
colincornaby Feb 18, 2024
6d45c88
Updating permissions for Mac patcher
colincornaby Mar 18, 2024
666df02
Updating xib deployment target
colincornaby Mar 18, 2024
5d111b9
Fixing delegate conformance
colincornaby Mar 18, 2024
d36917d
Reducing required flags
colincornaby Jun 17, 2024
f734138
Update Sources/Plasma/FeatureLib/pfPatcher/pfPatcher.cpp
colincornaby Jun 18, 2024
50f7d3d
Updating libarchive target include
colincornaby Jun 18, 2024
5d83030
Fixing client not terminating on patch error
colincornaby Jun 24, 2024
b0ef218
Passing error string through to NSError
colincornaby Jun 24, 2024
ab6d0b4
Adding executable suffix
colincornaby Jul 1, 2024
ea2c595
Fixes for self patch
colincornaby Jul 1, 2024
f82cf31
Cleaning up executable suffix code
colincornaby Jul 1, 2024
9d5e76f
Style cleanup
colincornaby Jul 9, 2024
6045bf8
Removing custom LibArchive check
colincornaby Jul 9, 2024
dcd06d9
Apply suggestions from code review
colincornaby Jul 10, 2024
1e4cb16
Cleaning up leak on error
colincornaby Jul 10, 2024
7dc5c44
Apply suggestions from code review
colincornaby Jul 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -246,6 +246,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;
dpogue marked this conversation as resolved.
Show resolved Hide resolved

@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;
colincornaby marked this conversation as resolved.
Show resolved Hide resolved
}

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);
dpogue marked this conversation as resolved.
Show resolved Hide resolved
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;
colincornaby marked this conversation as resolved.
Show resolved Hide resolved
}

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;
colincornaby marked this conversation as resolved.
Show resolved Hide resolved
}

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