Compare commits
2
Commits
0.1
..
backport-rac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c89431c4a | ||
|
|
2e33ff84f9 |
Vendored
+1
-1
Submodule External/Mantle updated: fe3a65987b...8aef509d18
Vendored
+1
-1
Submodule External/OHHTTPStubs updated: 08776e99bc...acaf8b8af7
Vendored
+1
-1
Submodule External/ReactiveCocoa updated: 16c181bbe8...09e17b9b9b
@@ -26,16 +26,13 @@ updates installed are valid.
|
||||
1. Install xctool with `brew install xctool`
|
||||
1. Add the Squirrel repository as a git submodule
|
||||
1. Run `script/bootstrap` from within the submodule
|
||||
1. Add references to Squirrel.xcodeproj and its [dependencies](#dependencies) to
|
||||
your project
|
||||
1. Add a reference to Squirrel.xcodeproj to your project
|
||||
1. Add Squirrel.framework as a target dependency
|
||||
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
|
||||
it into your Frameworks directory
|
||||
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
|
||||
does not embed them itself.
|
||||
|
||||
If you’re developing Squirrel on its own, then use `Squirrel.xcworkspace`.
|
||||
|
||||
# Dependencies
|
||||
|
||||
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
|
||||
|
||||
+565
-125
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0510"
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -29,7 +29,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
BuildableName = "SquirrelTests.xctest"
|
||||
BuildableName = "SquirrelTests.octest"
|
||||
BlueprintName = "SquirrelTests"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -47,7 +47,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
BuildableName = "SquirrelTests.xctest"
|
||||
BuildableName = "SquirrelTests.octest"
|
||||
BlueprintName = "SquirrelTests"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0510"
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0510"
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Squirrel.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/Mantle/Mantle.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/ReactiveCocoa/ReactiveCocoaFramework/ReactiveCocoa.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/ReactiveCocoa/external/specta/Specta.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/ReactiveCocoa/external/expecta/Expecta.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -21,44 +21,32 @@ extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// Implements the verification of Apple code signatures and requirements.
|
||||
// Implements the verification of Apple code signatures.
|
||||
@interface SQRLCodeSignature : MTLModel
|
||||
|
||||
// A serialized version of the `SecRequirementRef` that the receiver was
|
||||
// initialized with.
|
||||
@property (nonatomic, copy, readonly) NSData *requirementData;
|
||||
|
||||
// Determines the designated requirement of the currently-executing application.
|
||||
// The returned code signature can be used to verify that a bundle is code sign
|
||||
// valid and meets the designated requirement of the current application.
|
||||
// Determines the code signature of the currently-executing application.
|
||||
//
|
||||
// error - If not NULL, set to any error that occurs.
|
||||
//
|
||||
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
|
||||
// designated requirement for the running code.
|
||||
// signature for the running code.
|
||||
+ (instancetype)currentApplicationSignature:(NSError **)error;
|
||||
|
||||
// Determines the designated requirement of the specified bundle. The returned
|
||||
// code signature can be used to verify that an arbitrary bundle is valid and
|
||||
// meets the designated requirement of `bundle`.
|
||||
// Initializes the receiver with the given requirement.
|
||||
//
|
||||
// bundleURL - Must not be nil, the location of a bundle directory structure
|
||||
// which has been code signed and includes a designated requirement.
|
||||
// This bundle's designated requirement is used when verifying other
|
||||
// bundles.
|
||||
// error - If not NULL, set to any error that occurs.
|
||||
//
|
||||
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
|
||||
// designated requirement of the bundle at `bundleURL`.
|
||||
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)error;
|
||||
// requirement - The code requirement for tested bundles. This must not be NULL.
|
||||
- (id)initWithRequirement:(SecRequirementRef)requirement;
|
||||
|
||||
// Verifies the code signature of the specified bundle and verifies that the
|
||||
// bundle meets the receiver's requirement.
|
||||
// Verifies that the code signature of the specified bundle matches the receiver.
|
||||
//
|
||||
// bundleURL - The URL to the bundle to verify on disk. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send completed on success, or error
|
||||
// if the requirement was not verified successfully.
|
||||
// if the code signature was not verified successfully.
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL;
|
||||
|
||||
@end
|
||||
|
||||
@@ -19,14 +19,9 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
@interface SQRLCodeSignature ()
|
||||
|
||||
// A `SecRequirementRef` that tested bundles must satisfy.
|
||||
@property (atomic, strong) id requirement;
|
||||
|
||||
// Initializes the receiver with the given requirement.
|
||||
//
|
||||
// This is the designated initializer for this class.
|
||||
//
|
||||
// requirement - The code requirement for tested bundles. This must not be NULL.
|
||||
- (id)initWithRequirement:(SecRequirementRef)requirement;
|
||||
// This property is automatically retained.
|
||||
@property (atomic) id requirement;
|
||||
|
||||
@end
|
||||
|
||||
@@ -42,49 +37,8 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
+ (instancetype)currentApplicationSignature:(NSError **)errorRef {
|
||||
SecCodeRef staticCode = NULL;
|
||||
OSStatus error = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
|
||||
if (error != noErr) {
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@onExit {
|
||||
CFRelease(staticCode);
|
||||
};
|
||||
|
||||
return [self signatureWithCode:staticCode error:errorRef];
|
||||
}
|
||||
|
||||
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)errorRef {
|
||||
SecStaticCodeRef bundleCode = NULL;
|
||||
OSStatus error = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &bundleCode);
|
||||
if (error != noErr) {
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@onExit {
|
||||
CFRelease(bundleCode);
|
||||
};
|
||||
|
||||
return [self signatureWithCode:bundleCode error:errorRef];
|
||||
}
|
||||
|
||||
+ (instancetype)signatureWithCode:(SecStaticCodeRef)code error:(NSError **)errorRef {
|
||||
SecRequirementRef designatedRequirement = NULL;
|
||||
OSStatus error = SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &designatedRequirement);
|
||||
if (error != noErr) {
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@onExit {
|
||||
CFRelease(designatedRequirement);
|
||||
};
|
||||
|
||||
return [[SQRLCodeSignature alloc] initWithRequirement:designatedRequirement];
|
||||
+ (instancetype)currentApplicationSignature:(NSError **)error {
|
||||
return [self modelWithDictionary:nil error:error];
|
||||
}
|
||||
|
||||
- (id)initWithRequirement:(SecRequirementRef)requirement {
|
||||
@@ -95,6 +49,35 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionary error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
if (self.requirement == nil) {
|
||||
SecCodeRef staticCode = NULL;
|
||||
OSStatus result = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
|
||||
@onExit {
|
||||
if (staticCode != NULL) CFRelease(staticCode);
|
||||
};
|
||||
|
||||
if (result != noErr) {
|
||||
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
SecRequirementRef req = NULL;
|
||||
result = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &req);
|
||||
self.requirement = CFBridgingRelease(req);
|
||||
|
||||
if (result != noErr) {
|
||||
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Verification
|
||||
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL {
|
||||
|
||||
@@ -13,9 +13,6 @@
|
||||
// Provides the file locations that Squirrel/ShipIt use.
|
||||
@interface SQRLDirectoryManager : NSObject
|
||||
|
||||
// The application identifier to use in file locations.
|
||||
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
|
||||
|
||||
// Returns the shared `SQRLDirectoryManager` for the running application, based
|
||||
// on the bundle identifier or application name.
|
||||
+ (instancetype)currentApplicationManager;
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@interface SQRLDirectoryManager ()
|
||||
|
||||
// The application identifier to use in file locations.
|
||||
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLDirectoryManager
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// SQRLInstaller+Private.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLInstaller.h"
|
||||
|
||||
// The defaults key to store a `SQRLInstallerOwnedBundle` so that a moved bundle
|
||||
// can be restored.
|
||||
extern NSString * const SQRLInstallerOwnedBundleKey;
|
||||
|
||||
// The defaults key to store the number of installation attempts that have been
|
||||
// made.
|
||||
extern NSString * const SQRLShipItInstallationAttemptsKey;
|
||||
+11
-16
@@ -11,7 +11,7 @@
|
||||
// The domain for errors originating within SQRLInstaller.
|
||||
extern NSString * const SQRLInstallerErrorDomain;
|
||||
|
||||
// There was an error copying the target or update bundle to a backup location.
|
||||
// There was an error copying the target bundle to the backup location.
|
||||
extern const NSInteger SQRLInstallerErrorBackupFailed;
|
||||
|
||||
// There was an error replacing the target bundle with the update.
|
||||
@@ -34,34 +34,25 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
|
||||
// There was an error moving a bundle across volumes.
|
||||
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
|
||||
|
||||
// There was an error changing the file permissions of the update.
|
||||
extern const NSInteger SQRLInstallerErrorChangingPermissions;
|
||||
|
||||
@class RACCommand;
|
||||
@class SQRLDirectoryManager;
|
||||
|
||||
// Performs the installation of an update, saving its intermediate state to user
|
||||
// defaults.
|
||||
// Performs the installation of an update, using the `SQRLShipItState` on disk,
|
||||
// as located by `SQRLDirectoryManager`.
|
||||
//
|
||||
// This class is meant to be used only after the app that will be updated has
|
||||
// terminated.
|
||||
@interface SQRLInstaller : NSObject
|
||||
|
||||
// Initializes an installer using the given application identifier, which is
|
||||
// used to scope resumable state stored to user defaults.
|
||||
//
|
||||
// applicationIdentifier - The defaults domain in which to store resumable
|
||||
// state. Must not be nil.
|
||||
- (instancetype)initWithApplicationIdentifier:(NSString *)applicationIdentifier;
|
||||
|
||||
// When executed with a `SQRLShipItRequest`, attempts to install the update or
|
||||
// When executed with a `SQRLShipItState`, attempts to install the update or
|
||||
// resume an in-progress installation.
|
||||
//
|
||||
// Each execution will complete or error on an unspecified scheduler when
|
||||
// installation has completed or failed.
|
||||
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
|
||||
|
||||
// When executed with a `SQRLShipItRequest`, aborts an installation, and
|
||||
// attempts to restore the old version of the application if necessary.
|
||||
// When executed with a `SQRLShipItState`, aborts an installation, and attempts
|
||||
// to restore the old version of the application if necessary.
|
||||
//
|
||||
// This must not be executed while `installUpdateCommand` is executing.
|
||||
//
|
||||
@@ -69,4 +60,8 @@ extern const NSInteger SQRLInstallerErrorChangingPermissions;
|
||||
// aborting/recovery has finished.
|
||||
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
|
||||
|
||||
// Initializes an installer using the given directory manager to read and write the
|
||||
// state of the installation.
|
||||
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
|
||||
|
||||
@end
|
||||
|
||||
+349
-334
@@ -12,13 +12,12 @@
|
||||
#import "RACSignal+SQRLTransactionExtensions.h"
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLTerminationListener.h"
|
||||
#import <libkern/OSAtomic.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <sys/xattr.h>
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
|
||||
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
|
||||
|
||||
@@ -29,58 +28,85 @@ const NSInteger SQRLInstallerErrorInvalidBundleVersion = -4;
|
||||
const NSInteger SQRLInstallerErrorMissingInstallationData = -5;
|
||||
const NSInteger SQRLInstallerErrorInvalidState = -6;
|
||||
const NSInteger SQRLInstallerErrorMovingAcrossVolumes = -7;
|
||||
const NSInteger SQRLInstallerErrorChangingPermissions = -8;
|
||||
|
||||
NSString * const SQRLShipItInstallationAttemptsKey = @"SQRLShipItInstallationAttempts";
|
||||
NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
// Maps an installer state to a selector to invoke.
|
||||
typedef struct {
|
||||
// The state for which the associated method should be invoked.
|
||||
SQRLInstallerState installerState;
|
||||
|
||||
// A method accepting a `SQRLShipItState` argument and returning a cold
|
||||
// signal.
|
||||
//
|
||||
// If NULL, installation should complete.
|
||||
SEL selector;
|
||||
} SQRLInstallerDispatchTableEntry;
|
||||
|
||||
@interface SQRLInstaller ()
|
||||
|
||||
// The defaults domain to store all resumable state in.
|
||||
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
|
||||
// Finds the state file to read and write from.
|
||||
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
|
||||
|
||||
// The bundle currently owned by this installer.
|
||||
// Reads the given key from `state`, failing if it's not set.
|
||||
//
|
||||
// Stores the bundle moved aside by an install request so that the original
|
||||
// bundle can be restored to its original location if needed.
|
||||
@property (atomic, strong) SQRLInstallerOwnedBundle *ownedBundle;
|
||||
|
||||
// Reads the given key from `request`, failing if it's not set.
|
||||
//
|
||||
// key - The property key to read from `request`. This must not be nil, and
|
||||
// should refer to a property of object type.
|
||||
// request - The request object to read. This must not be nil.
|
||||
// key - The property key to read from `state`. This must not be nil, and
|
||||
// should refer to a property of object type.
|
||||
// state - The state object to read. This must not be nil.
|
||||
//
|
||||
// Returns a signal which synchronously sends the non-nil read value then
|
||||
// completes, or errors.
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request;
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state;
|
||||
|
||||
// Moves the updateBundleURL to an owned directory to prevent symlink attack,
|
||||
// takes user:group ownership of the bundle, then verifies that it meets the
|
||||
// designated requirement of the targetBundleURL.
|
||||
// Performs the remaining stages of installation, as specified by `state`.
|
||||
//
|
||||
// request - The request whose update should be prepared and validated.
|
||||
// state - The installation state. This must not be nil.
|
||||
//
|
||||
// Returns a signal which sends the owned & validated bundle URL then completes,
|
||||
// or errors.
|
||||
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request;
|
||||
// Returns a signal which will complete or error on an unspecified thread.
|
||||
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state;
|
||||
|
||||
// Saves a `SQRLInstallerOwnedBundle` for the targetBundleURL to the
|
||||
// preferences, then moves the targetBundleURL to an owned directory.
|
||||
// Moves the specified bundle to a backup location.
|
||||
//
|
||||
// request - The request whose target should be removed in preparation of an
|
||||
// update being installed.
|
||||
// bundleURL - The URL to the bundle that should be backed up. This must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a signal which completes, or errors.
|
||||
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request;
|
||||
// Returns a signal which will send the `NSURL` to the proposed backup location
|
||||
// as soon as possible, then complete once the bundle has actually been moved.
|
||||
- (RACSignal *)backUpBundleAtURL:(NSURL *)bundleURL;
|
||||
|
||||
// Deletes a bundle that was moved into place using -moveAndTakeOwnershipOfBundleAtURL:.
|
||||
// Deletes a bundle that was backed up using -backUpBundleAtURL:.
|
||||
//
|
||||
// bundleURL - The URL to the backup bundle, as sent from -moveAndTakeOwnershipOfBundleAtURL:.
|
||||
// backupURL - The URL to the backup bundle, as sent from -backUpBundleAtURL:.
|
||||
// This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL;
|
||||
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL;
|
||||
|
||||
// Validates the code signature of a bundle, optionally restoring it upon
|
||||
// failure.
|
||||
//
|
||||
// bundleURL - The URL of the bundle whose code signature should be
|
||||
// verified. This must not be nil.
|
||||
// signature - The code signature that the bundle must match. This must
|
||||
// not be nil.
|
||||
// backupBundleURL - If not nil, the URL to a bundle that should replace
|
||||
// `bundleURL` if the code signature does not pass validation.
|
||||
//
|
||||
// Returns a signal which will synchronously complete if `bundleURL` passes
|
||||
// validation, or if the bundle was recovered from `backupBundleURL`. If
|
||||
// validation or recovery fails, an error will be sent.
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL;
|
||||
|
||||
// Attempts to determine whether a bundle has already been moved on disk.
|
||||
//
|
||||
// sourceURL - The original URL to the bundle. This must not be nil.
|
||||
// targetURL - The proposed destination URL for the bundle. This must not be
|
||||
// nil.
|
||||
// signature - The code signature that any item must match in order to be
|
||||
// considered the correct bundle. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send YES if `targetURL` points to
|
||||
// a bundle matching the code signature, NO if it doesn't and `sourceURL` still
|
||||
// exists, or an error otherwise.
|
||||
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature;
|
||||
|
||||
// Moves `sourceURL` to `targetURL`.
|
||||
//
|
||||
@@ -92,7 +118,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
// sourceURL - The URL to move from. This must not be nil.
|
||||
//
|
||||
// Retruns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
|
||||
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
|
||||
|
||||
// Recursively clears the quarantine extended attribute from the given
|
||||
// directory.
|
||||
@@ -106,28 +132,19 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
// Returns a signal which will send completed or error on a background thread.
|
||||
- (RACSignal *)clearQuarantineForDirectory:(NSURL *)directory;
|
||||
|
||||
// Recursively changes the owner and group of the given directory tree to that
|
||||
// of the current process, then disables writing for anyone but the owner.
|
||||
//
|
||||
// directoryURL - The URL to the folder to take ownership of. This must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLInstaller
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithApplicationIdentifier:(NSString *)applicationIdentifier {
|
||||
NSParameterAssert(applicationIdentifier != nil);
|
||||
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
|
||||
NSParameterAssert(directoryManager != nil);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_applicationIdentifier = [applicationIdentifier copy];
|
||||
_directoryManager = directoryManager;
|
||||
|
||||
@weakify(self);
|
||||
|
||||
@@ -139,63 +156,42 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
switchToLatest]
|
||||
setNameWithFormat:@"aborting"];
|
||||
|
||||
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItRequest *request) {
|
||||
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItState *state) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(request != nil);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
// Request can be changed between launches, the installer may have
|
||||
// already have an owned bundle, for a previous targetURL.
|
||||
//
|
||||
// If that's the case, we need to abort the previous owned bundle, and
|
||||
// then handle the new install request.
|
||||
|
||||
return [[[[self
|
||||
abortInstall]
|
||||
doError:^(NSError *error) {
|
||||
NSLog(@"Couldn't abort install and restore owned bundle to previous location %@, error %@", self.ownedBundle.originalURL, error.sqrl_verboseDescription);
|
||||
}]
|
||||
catchTo:[RACSignal empty]]
|
||||
then:^{
|
||||
return [self installRequest:request];
|
||||
}];
|
||||
return [[self
|
||||
resumeInstallationFromState:state]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
|
||||
}];
|
||||
|
||||
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItRequest *request) {
|
||||
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItState *state) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [self abortInstall];
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
|
||||
] reduce:^(NSURL *targetBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [self verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:state.backupBundleURL];
|
||||
}]
|
||||
flatten]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Preferences
|
||||
#pragma mark Installer State
|
||||
|
||||
- (SQRLInstallerOwnedBundle *)ownedBundle {
|
||||
id archiveData = CFBridgingRelease(CFPreferencesCopyValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost));
|
||||
if (![archiveData isKindOfClass:NSData.class]) return nil;
|
||||
|
||||
SQRLInstallerOwnedBundle *ownedBundle = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
|
||||
if (![ownedBundle isKindOfClass:SQRLInstallerOwnedBundle.class]) return nil;
|
||||
|
||||
return ownedBundle;
|
||||
}
|
||||
|
||||
- (void)setOwnedBundle:(SQRLInstallerOwnedBundle *)ownedBundle {
|
||||
NSData *archiveData = (ownedBundle == nil ? nil : [NSKeyedArchiver archivedDataWithRootObject:ownedBundle]);
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFPropertyListRef)archiveData, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
CFPreferencesSynchronize((__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request {
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(key != nil);
|
||||
NSParameterAssert(request != nil);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
id value = [request valueForKey:key];
|
||||
id value = [state valueForKey:key];
|
||||
if (value == nil) {
|
||||
NSString *errorDescription = [NSString stringWithFormat:NSLocalizedString(@"Missing %@", nil), key];
|
||||
return [RACSignal error:[self missingDataErrorWithDescription:errorDescription]];
|
||||
@@ -203,162 +199,249 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
return [RACSignal return:value];
|
||||
}
|
||||
}]
|
||||
setNameWithFormat:@"%@ -getRequiredKey: %@ fromRequest: %@", self, key, request];
|
||||
setNameWithFormat:@"%@ -getRequiredKey: %@ fromState: %@", self, key, state];
|
||||
}
|
||||
|
||||
#pragma mark Installer States
|
||||
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
const SQRLInstallerDispatchTableEntry dispatchTablePrototype[] = {
|
||||
{ .installerState = SQRLInstallerStateNothingToDo, .selector = NULL },
|
||||
{ .installerState = SQRLInstallerStateClearingQuarantine, .selector = @selector(clearQuarantineWithState:) },
|
||||
{ .installerState = SQRLInstallerStateBackingUp, .selector = @selector(backUpWithState:) },
|
||||
{ .installerState = SQRLInstallerStateInstalling, .selector = @selector(installWithState:) },
|
||||
{ .installerState = SQRLInstallerStateVerifyingInPlace, .selector = @selector(verifyInPlaceWithState:) },
|
||||
};
|
||||
|
||||
return [[[[[[[self
|
||||
ownedTemporaryDirectoryURL]
|
||||
flattenMap:^(NSURL *directoryURL) {
|
||||
return [self copyBundleAtURL:request.updateBundleURL toDirectory:directoryURL];
|
||||
}]
|
||||
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
|
||||
NSValue *boxedDispatchTable = [NSValue valueWithBytes:dispatchTablePrototype objCType:@encode(__typeof__(dispatchTablePrototype))];
|
||||
|
||||
RACSignal *step = [RACSignal defer:^{
|
||||
SQRLInstallerDispatchTableEntry dispatchTable[tableCount];
|
||||
[boxedDispatchTable getValue:&dispatchTable];
|
||||
|
||||
SQRLInstallerState installerState = state.installerState;
|
||||
|
||||
size_t tableIndex;
|
||||
for (tableIndex = 0; tableIndex < tableCount; tableIndex++) {
|
||||
if (dispatchTable[tableIndex].installerState == installerState) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tableIndex >= tableCount) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Invalid installer state %i", nil), (int)installerState],
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
|
||||
}
|
||||
|
||||
SEL selector = dispatchTable[tableIndex].selector;
|
||||
if (selector == NULL) {
|
||||
// Nothing to do.
|
||||
return [RACSignal empty];
|
||||
}
|
||||
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
|
||||
invocation.target = self;
|
||||
invocation.selector = selector;
|
||||
|
||||
SQRLShipItState *stateArg = state;
|
||||
[invocation setArgument:&stateArg atIndex:2];
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained RACSignal *step = nil;
|
||||
[invocation getReturnValue:&step];
|
||||
|
||||
SQRLInstallerState nextState;
|
||||
if (tableIndex + 1 >= tableCount) {
|
||||
nextState = SQRLInstallerStateNothingToDo;
|
||||
} else {
|
||||
nextState = dispatchTable[tableIndex + 1].installerState;
|
||||
}
|
||||
|
||||
return [[step
|
||||
doCompleted:^{
|
||||
NSLog(@"Completed state %i", (int)installerState);
|
||||
}]
|
||||
then:^{
|
||||
return [RACSignal return:@(nextState)];
|
||||
}];
|
||||
}];
|
||||
|
||||
return [[self
|
||||
stepRepeatedly:step withState:state]
|
||||
setNameWithFormat:@"%@ -resumeInstallationFromState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)stepRepeatedly:(RACSignal *)step withState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(step != nil);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [step flattenMap:^(NSNumber *nextState) {
|
||||
state.installerState = nextState.integerValue;
|
||||
state.installationStateAttempt = 1;
|
||||
return [[state
|
||||
writeUsingURL:self.directoryManager.shipItStateURL]
|
||||
// Automatically begin the next step.
|
||||
concat:[self stepRepeatedly:step withState:state]];
|
||||
}];
|
||||
}
|
||||
|
||||
- (RACSignal *)clearQuarantineWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[self
|
||||
getRequiredKey:@keypath(state.updateBundleURL) fromState:state]
|
||||
flattenMap:^(NSURL *bundleURL) {
|
||||
return [[[self
|
||||
clearQuarantineForDirectory:bundleURL]
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:bundleURL]];
|
||||
return [self clearQuarantineForDirectory:bundleURL];
|
||||
}]
|
||||
zipWith:[self codeSignatureForBundleAtURL:request.targetBundleURL]]
|
||||
reduceEach:^(NSURL *updateBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[[self
|
||||
verifyBundleAtURL:updateBundleURL usingSignature:codeSignature]
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:updateBundleURL]];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -prepareAndValidateUpdateBundleURLForRequest: %@", self, request];
|
||||
setNameWithFormat:@"%@ -clearQuarantineWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
- (RACSignal *)backUpWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
zip:@[
|
||||
[self ownedTemporaryDirectoryURL],
|
||||
[self codeSignatureForBundleAtURL:request.targetBundleURL],
|
||||
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
|
||||
NSURL *targetBundleURL = request.targetBundleURL;
|
||||
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
|
||||
|
||||
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
|
||||
}]
|
||||
doNext:^(SQRLInstallerOwnedBundle *ownedBundle) {
|
||||
self.ownedBundle = ownedBundle;
|
||||
}]
|
||||
flattenMap:^(SQRLInstallerOwnedBundle *ownedBundle) {
|
||||
return [self installItemToURL:ownedBundle.temporaryURL fromURL:ownedBundle.originalURL];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request];
|
||||
}
|
||||
|
||||
- (RACSignal *)installRequest:(SQRLShipItRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
return [[[[self
|
||||
prepareAndValidateUpdateBundleURLForRequest:request]
|
||||
flattenMap:^(NSURL *updateBundleURL) {
|
||||
return [[[[[[[self
|
||||
acquireTargetBundleURLForRequest:request]
|
||||
concat:[self installItemToURL:request.targetBundleURL fromURL:updateBundleURL]]
|
||||
concat:[RACSignal return:request.updateBundleURL]]
|
||||
concat:[RACSignal return:updateBundleURL]]
|
||||
concat:[RACSignal defer:^{
|
||||
return [RACSignal return:self.ownedBundle.temporaryURL];
|
||||
}]]
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [[[self
|
||||
deleteOwnedBundleAtURL:location]
|
||||
doError:^(NSError *error) {
|
||||
NSLog(@"Couldn't remove owned bundle at location %@, error %@", location, error.sqrl_verboseDescription);
|
||||
}]
|
||||
catchTo:[RACSignal empty]];
|
||||
}]
|
||||
doCompleted:^{
|
||||
self.ownedBundle = nil;
|
||||
}];
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path]
|
||||
setNameWithFormat:@"%@ -installRequest: %@", self, request];
|
||||
}
|
||||
|
||||
- (RACSignal *)abortInstall {
|
||||
// The request may have been tampered with to select a new targetURL to
|
||||
// which the moved bundles should be restored.
|
||||
//
|
||||
// Discard the request parameters and restore the owned bundle to its
|
||||
// original location.
|
||||
|
||||
SQRLInstallerOwnedBundle *ownedBundle = self.ownedBundle;
|
||||
if (ownedBundle == nil) return [RACSignal empty];
|
||||
|
||||
return [[[[self
|
||||
installItemToURL:ownedBundle.originalURL fromURL:ownedBundle.temporaryURL]
|
||||
doCompleted:^{
|
||||
self.ownedBundle = nil;
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), ownedBundle.originalURL.path]
|
||||
setNameWithFormat:@"%@ -abortInstall", self];
|
||||
}
|
||||
|
||||
#pragma mark Bundle Ownership
|
||||
|
||||
- (RACSignal *)ownedTemporaryDirectoryURL {
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSString *tmpPath = [NSTemporaryDirectory() stringByResolvingSymlinksInPath];
|
||||
NSString *template = [NSString stringWithFormat:@"%@.XXXXXXXX", self.applicationIdentifier];
|
||||
|
||||
char *fullTemplate = strdup([tmpPath stringByAppendingPathComponent:template].UTF8String);
|
||||
@onExit {
|
||||
free(fullTemplate);
|
||||
};
|
||||
|
||||
if (mkdtemp(fullTemplate) == NULL) {
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state],
|
||||
] reduce:^(NSURL *bundleURL, SQRLCodeSignature *codeSignature) {
|
||||
RACSignal *skipBackup = [RACSignal return:@NO];
|
||||
if (state.backupBundleURL != nil) {
|
||||
skipBackup = [self checkWhetherBundlePreviouslyAtURL:bundleURL wasInstalledAtURL:state.backupBundleURL usingSignature:codeSignature];
|
||||
}
|
||||
|
||||
NSURL *URL = [NSURL fileURLWithPath:[NSFileManager.defaultManager stringWithFileSystemRepresentation:fullTemplate length:strlen(fullTemplate)] isDirectory:YES];
|
||||
return [RACSignal return:URL];
|
||||
return [skipBackup flattenMap:^(NSNumber *skip) {
|
||||
if (skip.boolValue) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
return [self backUpBundleAtURL:bundleURL];
|
||||
}
|
||||
}];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create temporary folder", nil)];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
flatten]
|
||||
flattenMap:^(NSURL *backupBundleURL) {
|
||||
// Save the chosen backup URL as soon as we have it, so we
|
||||
// can resume even if the state change hasn't taken effect.
|
||||
//
|
||||
// N.B. It's important that this method remain
|
||||
// synchronous, so it finishes before returning
|
||||
// control to -backUpBundleAtURL:. Really, the flow
|
||||
// here should be refactored so it doesn't matter.
|
||||
state.backupBundleURL = backupBundleURL;
|
||||
return [state writeUsingURL:self.directoryManager.shipItStateURL];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -ownedDirectoryURL", self];
|
||||
setNameWithFormat:@"%@ -backUpWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)copyBundleAtURL:(NSURL *)bundleURL toDirectory:(NSURL *)directoryURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
NSParameterAssert(directoryURL != nil);
|
||||
- (RACSignal *)installWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:bundleURL.lastPathComponent];
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.updateBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
|
||||
] reduce:^(NSURL *targetBundleURL, NSURL *updateBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[[[self
|
||||
checkWhetherBundlePreviouslyAtURL:updateBundleURL wasInstalledAtURL:targetBundleURL usingSignature:codeSignature]
|
||||
flattenMap:^(NSNumber *skip) {
|
||||
if (skip.boolValue) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
return [self installItemAtURL:targetBundleURL fromURL:updateBundleURL];
|
||||
}
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to replace bundle %@ with update %@", nil), targetBundleURL, updateBundleURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorReplacingTarget toError:error]];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
// Verify that the target bundle didn't get corrupted during
|
||||
// failure. Try recovering it if it did.
|
||||
return [[self
|
||||
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
|
||||
then:^{
|
||||
// Recovery succeeded, but we still want to pass
|
||||
// through the original error.
|
||||
return [RACSignal error:error];
|
||||
}];
|
||||
}];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -installWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)verifyInPlaceWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
|
||||
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[self
|
||||
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
|
||||
then:^{
|
||||
return [[self
|
||||
deleteBackupAtURL:backupBundleURL]
|
||||
catchTo:[RACSignal empty]];
|
||||
}];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
|
||||
}
|
||||
|
||||
#pragma mark Backing Up
|
||||
|
||||
- (RACSignal *)backUpBundleAtURL:(NSURL *)targetBundleURL {
|
||||
NSParameterAssert(targetBundleURL != nil);
|
||||
|
||||
return [[[[[[self.directoryManager
|
||||
applicationSupportURL]
|
||||
flattenMap:^(NSURL *applicationSupportURL) {
|
||||
NSError *error = nil;
|
||||
NSURL *temporaryDirectoryURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:applicationSupportURL create:YES error:&error];
|
||||
if (temporaryDirectoryURL == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:temporaryDirectoryURL];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create backup folder", nil)];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
map:^(NSURL *temporaryDirectoryURL) {
|
||||
return [temporaryDirectoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
|
||||
}]
|
||||
flattenMap:^(NSURL *backupBundleURL) {
|
||||
return [[[[self
|
||||
installItemAtURL:backupBundleURL fromURL:targetBundleURL]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to move bundle %@ to backup location %@", nil), targetBundleURL, backupBundleURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
ignoreValues]
|
||||
// Return the backup URL before doing any work, to increase
|
||||
// fault tolerance.
|
||||
startWith:backupBundleURL];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -backUpBundleAtURL: %@", self, targetBundleURL];
|
||||
}
|
||||
|
||||
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
|
||||
NSParameterAssert(backupURL != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
BOOL copy = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:newBundleURL error:&error];
|
||||
if (!copy) return [RACSignal error:error];
|
||||
|
||||
return [RACSignal return:newBundleURL];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to copy bundle %@ to directory %@", nil), bundleURL, newBundleURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -copyBundleAtURL: %@ toDirectory: %@", self, bundleURL, directoryURL];
|
||||
}
|
||||
|
||||
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
if ([NSFileManager.defaultManager removeItemAtURL:bundleURL error:&error]) {
|
||||
NSError *error = nil;
|
||||
if ([NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
return [RACSignal error:error];
|
||||
@@ -366,7 +449,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
}]
|
||||
then:^{
|
||||
// Also remove the temporary directory that the backup lived in.
|
||||
NSURL *temporaryDirectoryURL = bundleURL.URLByDeletingLastPathComponent;
|
||||
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
|
||||
|
||||
// However, use rmdir() to skip it in case there are other files
|
||||
// contained within (for whatever reason).
|
||||
@@ -376,37 +459,58 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
|
||||
}
|
||||
}]
|
||||
setNameWithFormat:@"%@ -deleteOwnedBundleAtURL: %@", self, bundleURL];
|
||||
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
|
||||
}
|
||||
|
||||
#pragma mark Verification
|
||||
|
||||
- (RACSignal *)codeSignatureForBundleAtURL:(NSURL *)URL {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
SQRLCodeSignature *codeSignature = [SQRLCodeSignature signatureWithBundle:URL error:&error];
|
||||
if (codeSignature == nil) return [RACSignal error:error];
|
||||
return [RACSignal return:codeSignature];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -codeSignatureForBundleAtURL: %@", self, URL];
|
||||
}
|
||||
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature {
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
NSParameterAssert(signature != nil);
|
||||
|
||||
return [[[self
|
||||
takeOwnershipOfDirectory:bundleURL]
|
||||
then:^{
|
||||
return [signature verifyBundleAtURL:bundleURL];
|
||||
return [[[signature
|
||||
verifyBundleAtURL:bundleURL]
|
||||
catch:^(NSError *error) {
|
||||
if (backupBundleURL == nil) return [RACSignal error:error];
|
||||
|
||||
return [[[[self
|
||||
installItemAtURL:bundleURL fromURL:backupBundleURL]
|
||||
initially:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
|
||||
}]
|
||||
doCompleted:^{
|
||||
NSLog(@"Restored backup bundle to %@", bundleURL);
|
||||
}]
|
||||
doError:^(NSError *recoveryError) {
|
||||
NSLog(@"Could not restore backup bundle %@ to %@: %@", backupBundleURL, bundleURL, recoveryError.sqrl_verboseDescription);
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@", self, bundleURL, signature];
|
||||
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@ recoveringUsingBackupAtURL: %@", self, bundleURL, signature, backupBundleURL];
|
||||
}
|
||||
|
||||
#pragma mark Installation
|
||||
|
||||
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature {
|
||||
NSParameterAssert(targetURL != nil);
|
||||
NSParameterAssert(sourceURL != nil);
|
||||
NSParameterAssert(signature != nil);
|
||||
|
||||
return [[[[self
|
||||
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
|
||||
then:^{
|
||||
return [RACSignal return:@YES];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
BOOL directory;
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
|
||||
// If the source still exists, this isn't an error.
|
||||
return [RACSignal return:@NO];
|
||||
} else {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}]
|
||||
setNameWithFormat:@"%@ -checkWhetherBundlePreviouslyAtURL: %@ wasInstalledAtURL: %@", self, sourceURL, targetURL];
|
||||
}
|
||||
|
||||
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
NSParameterAssert(targetURL != nil);
|
||||
NSParameterAssert(sourceURL != nil);
|
||||
|
||||
@@ -483,95 +587,6 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
setNameWithFormat:@"%@ -clearQuarantineForDirectory: %@", self, directory];
|
||||
}
|
||||
|
||||
#pragma mark File Security
|
||||
|
||||
- (RACSignal *)readFileSecurityOfURL:(NSURL *)location {
|
||||
NSParameterAssert(location != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
NSFileSecurity *fileSecurity;
|
||||
if (![location getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:fileSecurity];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -readFileSecurity: %@", self, location];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeFileSecurity:(NSFileSecurity *)fileSecurity toURL:(NSURL *)location {
|
||||
NSParameterAssert(location != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
if (![location setResourceValue:fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -writeFileSecurity: %@", self, location];
|
||||
}
|
||||
|
||||
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL {
|
||||
NSParameterAssert(directoryURL != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
createSignal:^(id<RACSubscriber> subscriber) {
|
||||
NSDirectoryEnumerator *enumerator = [NSFileManager.defaultManager enumeratorAtURL:directoryURL includingPropertiesForKeys:@[ NSURLFileSecurityKey ] options:0 errorHandler:^ BOOL (NSURL *url, NSError *error) {
|
||||
[subscriber sendError:error];
|
||||
return NO;
|
||||
}];
|
||||
|
||||
return [enumerator.rac_sequence.signal subscribe:subscriber];
|
||||
}]
|
||||
flattenMap:^(NSURL *itemURL) {
|
||||
return [[[self
|
||||
readFileSecurityOfURL:itemURL]
|
||||
flattenMap:^(NSFileSecurity *fileSecurity) {
|
||||
if (![self takeOwnershipOfFileSecurity:fileSecurity]) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Permissions Error", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Couldn’t update permissions of %@", nil), itemURL.path],
|
||||
NSURLErrorKey: itemURL
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorChangingPermissions userInfo:errorInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:fileSecurity];
|
||||
}]
|
||||
flattenMap:^(NSFileSecurity *fileSecurity) {
|
||||
return [self writeFileSecurity:fileSecurity toURL:itemURL];
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -takeOwnershipOfDirectory: %@", self, directoryURL];
|
||||
}
|
||||
|
||||
- (BOOL)takeOwnershipOfFileSecurity:(NSFileSecurity *)fileSecurity {
|
||||
CFFileSecurityRef actualFileSecurity = (__bridge CFFileSecurityRef)fileSecurity;
|
||||
|
||||
// If ShipIt is running as root, this will change the owner to
|
||||
// root:wheel.
|
||||
if (!CFFileSecuritySetOwner(actualFileSecurity, getuid())) return NO;
|
||||
if (!CFFileSecuritySetGroup(actualFileSecurity, getgid())) return NO;
|
||||
|
||||
mode_t fileMode = 0;
|
||||
if (!CFFileSecurityGetMode(actualFileSecurity, &fileMode)) return NO;
|
||||
|
||||
// Remove write permission from group and other, leave executable
|
||||
// bit as it was for both.
|
||||
//
|
||||
// Permissions will be r-(x?)r-(x?) afterwards, with owner
|
||||
// permissions left as is.
|
||||
fileMode = (fileMode & ~(S_IWGRP | S_IWOTH));
|
||||
|
||||
return CFFileSecuritySetMode(actualFileSecurity, fileMode);
|
||||
}
|
||||
|
||||
#pragma mark Error Handling
|
||||
|
||||
- (NSError *)missingDataErrorWithDescription:(NSString *)description {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// SQRLInstallerOwnedBundle.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
@class SQRLCodeSignature;
|
||||
|
||||
// Tracks original and temporary locations of a bundle. Should be created and
|
||||
// serialised before moving a bundle aside.
|
||||
//
|
||||
// Can be used to ensure new targetURL requests meet the original bundle at that
|
||||
// location's code signature, even though it's been moved aside.
|
||||
@interface SQRLInstallerOwnedBundle : MTLModel
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// originalURL - Where the bundle currently resides, and should be restored to
|
||||
// should an error occur.
|
||||
// temporaryURL - Where the bundle will be moved to, so that another bundle can
|
||||
// take its place at originalURL.
|
||||
// codeSignature - The code signature of the original bundle, so that the
|
||||
// signature can be used irrespective of where the bundle
|
||||
// currently resides.
|
||||
//
|
||||
// Returns an initialised owned bundle for serializing.
|
||||
- (instancetype)initWithOriginalURL:(NSURL *)originalURL temporaryURL:(NSURL *)temporaryURL codeSignature:(SQRLCodeSignature *)codeSignature;
|
||||
|
||||
@property (readonly, copy, nonatomic) NSURL *originalURL;
|
||||
@property (readonly, copy, nonatomic) NSURL *temporaryURL;
|
||||
@property (readonly, copy, nonatomic) SQRLCodeSignature *codeSignature;
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// SQRLInstallerOwnedBundle.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
|
||||
#import <ReactiveCocoa/EXTKeyPathCoding.h>
|
||||
|
||||
@implementation SQRLInstallerOwnedBundle
|
||||
|
||||
- (instancetype)initWithOriginalURL:(NSURL *)originalURL temporaryURL:(NSURL *)temporaryURL codeSignature:(SQRLCodeSignature *)codeSignature {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.originalURL): originalURL,
|
||||
@keypath(self.temporaryURL): temporaryURL,
|
||||
@keypath(self.codeSignature): codeSignature,
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,96 +0,0 @@
|
||||
//
|
||||
// SQRLShipItRequest.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// The domain for errors originating within `SQRLShipItRequest`.
|
||||
extern NSString * const SQRLShipItRequestErrorDomain;
|
||||
|
||||
// Errors originating from the `SQRLShipItRequestErrorDomain`.
|
||||
//
|
||||
// SQRLShipItRequestErrorMissingRequiredProperty - A required property was `nil`
|
||||
// upon initialization.
|
||||
//
|
||||
// The `userInfo` dictionary for
|
||||
// this error will contain
|
||||
// `SQRLShipItStatePropertyErrorKey`.
|
||||
//
|
||||
// SQRLShipItRequestErrorUnarchiving - The saved request could not
|
||||
// be unarchived, possibly
|
||||
// because it's invalid.
|
||||
//
|
||||
// SQRLShipItRequestErrorArchiving - The request object could not
|
||||
// be archived.
|
||||
typedef enum : NSInteger {
|
||||
SQRLShipItRequestErrorMissingRequiredProperty = 1,
|
||||
SQRLShipItRequestErrorUnarchiving = 2,
|
||||
SQRLShipItRequestErrorArchiving = 3,
|
||||
} SQRLShipItRequestError;
|
||||
|
||||
// Associated with an `NSString` indicating the required property key that did
|
||||
// not have a value upon initialization.
|
||||
extern NSString * const SQRLShipItRequestPropertyErrorKey;
|
||||
|
||||
// Constructed and written to disk for `ShipIt` to pick up. This represents a
|
||||
// single update request from the client's perspective.
|
||||
@interface SQRLShipItRequest : MTLModel
|
||||
|
||||
// Reads a `SQRLShipItState` from disk, at the location specified by the given
|
||||
// URL signal.
|
||||
//
|
||||
// URLSignal - Determines the file location to read from, the signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
|
||||
// complete, or error.
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URLSignal;
|
||||
|
||||
// Writes the receiver to disk, at the location specified by the given URL
|
||||
// signal.
|
||||
//
|
||||
// URLSignal - Determines the file location to write to. The signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URLSignal;
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// updateBundleURL - The update bundle which will replace
|
||||
// targetBundleURL. Must not be nil.
|
||||
// targetBundleURL - Where the update should be installed, if a bundle
|
||||
// is already present, the update is checked for
|
||||
// suitability against this bundle. Must not be nil.
|
||||
// bundleIdentifier - The bundle identifier that the installer should
|
||||
// wait for instances of to terminate before
|
||||
// installing. Can be nil.
|
||||
// launchAfterInstallation - Whether the updated application should be launched
|
||||
// after installation.
|
||||
//
|
||||
// Returns a request which can be written to disk for ShipIt to read and
|
||||
// perform.
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation;
|
||||
|
||||
// The URL to the downloaded update's app bundle.
|
||||
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
|
||||
|
||||
// The URL to the app bundle that should be replaced with the update.
|
||||
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
|
||||
|
||||
// The bundle identifier of the application being updated.
|
||||
//
|
||||
// If not nil, the installer will wait for applications matching this identifier
|
||||
// (and `targetBundleURL`) to terminate before continuing.
|
||||
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
|
||||
|
||||
// Whether to launch the application after an update is successfully installed.
|
||||
@property (nonatomic, assign, readonly) BOOL launchAfterInstallation;
|
||||
|
||||
@end
|
||||
@@ -1,192 +0,0 @@
|
||||
//
|
||||
// SQRLShipItRequest.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
NSString * const SQRLShipItRequestErrorDomain = @"SQRLShipItRequestErrorDomain";
|
||||
|
||||
NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestPropertyErrorKey";
|
||||
|
||||
@interface SQRLShipItRequest () <MTLJSONSerializing>
|
||||
@end
|
||||
|
||||
@implementation SQRLShipItRequest
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionary error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
|
||||
if ([self valueForKey:key] != nil) return YES;
|
||||
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorMissingRequiredProperty userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
};
|
||||
|
||||
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
|
||||
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.updateBundleURL): updateBundleURL,
|
||||
@keypath(self.targetBundleURL): targetBundleURL,
|
||||
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
|
||||
@keypath(self.launchAfterInstallation): @(launchAfterInstallation),
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)updateBundleURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)targetBundleURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[[URL
|
||||
flattenMap:^(NSURL *stateURL) {
|
||||
NSError *error;
|
||||
NSData *data = [self readFromURL:stateURL error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}]
|
||||
flattenMap:^(NSData *data) {
|
||||
return [self readFromData:data];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read update request", nil),
|
||||
};
|
||||
if (error != nil) {
|
||||
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
NSUnderlyingErrorKey: error,
|
||||
}];
|
||||
}
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorUnarchiving userInfo:userInfo]];
|
||||
}]
|
||||
setNameWithFormat:@"+readUsingURL: %@", URL];
|
||||
}
|
||||
|
||||
+ (NSData *)readFromURL:(NSURL *)URL error:(NSError **)errorRef {
|
||||
__block NSData *data = nil;
|
||||
|
||||
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
|
||||
[coordinator coordinateReadingItemAtURL:URL options:NSFileCoordinatorReadingWithoutChanges error:errorRef byAccessor:^(NSURL *newURL) {
|
||||
data = [NSData dataWithContentsOfURL:newURL options:NSDataReadingUncached error:errorRef];
|
||||
}];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (RACSignal *)readFromData:(NSData *)data {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
NSDictionary *JSONDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (JSONDictionary == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
SQRLShipItRequest *request = [MTLJSONAdapter modelOfClass:SQRLShipItRequest.class fromJSONDictionary:JSONDictionary error:&error];
|
||||
if (request == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:request];
|
||||
|
||||
}]
|
||||
setNameWithFormat:@"+readFromData: <NSData %p>", data];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
zip:@[
|
||||
URL,
|
||||
[self serialization]
|
||||
] reduce:^(NSURL *stateURL, NSData *data) {
|
||||
NSError *error;
|
||||
BOOL write = [self writeData:data toURL:stateURL error:&error];
|
||||
if (!write) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flatten]
|
||||
catch:^(NSError *error) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not write update request", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil),
|
||||
};
|
||||
if (error != nil) {
|
||||
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
NSUnderlyingErrorKey: error,
|
||||
}];
|
||||
}
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorArchiving userInfo:userInfo]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData *)data toURL:(NSURL *)URL error:(NSError **)errorRef {
|
||||
__block BOOL success = NO;
|
||||
|
||||
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
|
||||
[coordinator coordinateWritingItemAtURL:URL options:0 error:errorRef byAccessor:^(NSURL *newURL) {
|
||||
success = [data writeToURL:newURL options:NSDataWritingAtomic error:errorRef];
|
||||
}];
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (RACSignal *)serialization {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:self];
|
||||
|
||||
NSError *error;
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}]
|
||||
setNameWithFormat:@"%@ serialization", self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// SQRLShipItState.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-08.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
// The domain for errors originating within `SQRLShipItState`.
|
||||
extern NSString * const SQRLShipItStateErrorDomain;
|
||||
|
||||
// A required property was `nil` upon initialization.
|
||||
//
|
||||
// The `userInfo` dictionary for this error will contain
|
||||
// `SQRLShipItStatePropertyErrorKey`.
|
||||
extern const NSInteger SQRLShipItStateErrorMissingRequiredProperty;
|
||||
|
||||
// The saved state on disk could not be unarchived, possibly because it's
|
||||
// invalid.
|
||||
extern const NSInteger SQRLShipItStateErrorUnarchiving;
|
||||
|
||||
// The state object could not be archived.
|
||||
extern const NSInteger SQRLShipItStateErrorArchiving;
|
||||
|
||||
// Associated with an NSString indicating the required property key that did not
|
||||
// have a value upon initialization.
|
||||
extern NSString * const SQRLShipItStatePropertyErrorKey;
|
||||
|
||||
// The current state of the installer, for persistence across relaunches and for
|
||||
// tolerance of system failures.
|
||||
//
|
||||
// SQRLInstallerStateNothingToDo - ShipIt has not started installing yet.
|
||||
// SQRLInstallerStateClearingQuarantine - Clearing the quarantine flag on the
|
||||
// update bundle so it can used without
|
||||
// issue.
|
||||
// SQRLInstallerStateBackingUp - Backing up the target bundle so it can
|
||||
// be restored in the event of failure.
|
||||
// SQRLInstallerStateInstalling - Replacing the target bundle with the
|
||||
// update bundle.
|
||||
// SQRLInstallerStateVerifyingInPlace - Verifying that the target bundle is
|
||||
// still valid after updating.
|
||||
//
|
||||
// Note that these values must remain backwards compatible, so ShipIt doesn't
|
||||
// start up in a weird mode on a newer version.
|
||||
typedef enum : NSInteger {
|
||||
SQRLInstallerStateNothingToDo = 0,
|
||||
SQRLInstallerStateClearingQuarantine,
|
||||
SQRLInstallerStateBackingUp,
|
||||
SQRLInstallerStateInstalling,
|
||||
SQRLInstallerStateVerifyingInPlace,
|
||||
} SQRLInstallerState;
|
||||
|
||||
@class RACSignal;
|
||||
@class SQRLCodeSignature;
|
||||
|
||||
// Encapsulates all the state needed by the ShipIt process.
|
||||
@interface SQRLShipItState : MTLModel
|
||||
|
||||
// Reads a `SQRLShipItState` from disk, at the location specified by the given
|
||||
// URL signal.
|
||||
//
|
||||
// URLSignal - Determines the file location to read from, the signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send a `SQRLShipItState` then
|
||||
// complete, or error.
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL;
|
||||
|
||||
// Initializes the receiver with the arguments that will not change during
|
||||
// installation.
|
||||
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature;
|
||||
|
||||
// Writes the receiver to disk, at the location specified by the given URL
|
||||
// signal.
|
||||
//
|
||||
// URL - Determines the file location to write to. The signal should send an
|
||||
// `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL;
|
||||
|
||||
// The URL to the app bundle that should be replaced with an update.
|
||||
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
|
||||
|
||||
// The URL to the downloaded update's app bundle.
|
||||
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
|
||||
|
||||
// A code signature that the update bundle must match in order to be valid.
|
||||
@property (nonatomic, copy, readonly) SQRLCodeSignature *codeSignature;
|
||||
|
||||
// The bundle identifier of the application being updated.
|
||||
//
|
||||
// If not nil, the installer will wait for applications matching this identifier
|
||||
// (and `targetBundleURL`) to terminate before continuing.
|
||||
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
|
||||
|
||||
// The current state of the installer.
|
||||
@property (atomic, assign) SQRLInstallerState installerState;
|
||||
|
||||
// The number of installation attempts that have occurred for the current
|
||||
// `state`.
|
||||
@property (atomic, assign) NSUInteger installationStateAttempt;
|
||||
|
||||
// Whether to relaunch the application after an update is successfully
|
||||
// installed.
|
||||
@property (atomic, assign) BOOL relaunchAfterInstallation;
|
||||
|
||||
// The URL where the target bundle has been backed up to before installing the
|
||||
// update.
|
||||
//
|
||||
// This property is set automatically during the course of installation. It
|
||||
// should not be preset.
|
||||
@property (atomic, copy) NSURL *backupBundleURL;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// SQRLShipItState.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-08.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLShipItState.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
NSString * const SQRLShipItStateErrorDomain = @"SQRLShipItStateErrorDomain";
|
||||
NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErrorKey";
|
||||
|
||||
const NSInteger SQRLShipItStateErrorMissingRequiredProperty = 1;
|
||||
const NSInteger SQRLShipItStateErrorUnarchiving = 2;
|
||||
const NSInteger SQRLShipItStateErrorArchiving = 3;
|
||||
|
||||
@implementation SQRLShipItState
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionary error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
|
||||
if ([self valueForKey:key] != nil) return YES;
|
||||
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
};
|
||||
|
||||
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
|
||||
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
|
||||
if (!validateKey(@keypath(self.codeSignature))) return nil;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.targetBundleURL): targetBundleURL,
|
||||
@keypath(self.updateBundleURL): updateBundleURL,
|
||||
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
|
||||
@keypath(self.codeSignature): codeSignature,
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[URL
|
||||
flattenMap:^(NSURL *stateURL) {
|
||||
NSError *error = nil;
|
||||
NSData *data = [NSData dataWithContentsOfURL:stateURL options:NSDataReadingUncached error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}]
|
||||
flattenMap:^(NSData *data) {
|
||||
SQRLShipItState *state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
if (![state isKindOfClass:SQRLShipItState.class]) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read saved state", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:state];
|
||||
}]
|
||||
setNameWithFormat:@"+readUsingURL: %@", URL];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
RACSignal *serialization = [RACSignal defer:^{
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self];
|
||||
if (data == nil) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not save state", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}];
|
||||
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
URL,
|
||||
serialization
|
||||
] reduce:^(NSURL *stateURL, NSData *data) {
|
||||
NSError *error = nil;
|
||||
if (![data writeToURL:stateURL options:NSDataWritingAtomic error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,20 +6,6 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
// Represents the current state of the updater.
|
||||
//
|
||||
// SQRLUpdaterStateIdle - Doing absolutely diddly squat.
|
||||
// SQRLUpdaterStateCheckingForUpdate - Checking for any updates from the server.
|
||||
// SQRLUpdaterStateDownloadingUpdate - Update found, downloading the archive.
|
||||
// SQRLUpdaterStateAwaitingRelaunch - Awaiting a relaunch to install
|
||||
// the update.
|
||||
typedef enum : NSUInteger {
|
||||
SQRLUpdaterStateIdle,
|
||||
SQRLUpdaterStateCheckingForUpdate,
|
||||
SQRLUpdaterStateDownloadingUpdate,
|
||||
SQRLUpdaterStateAwaitingRelaunch,
|
||||
} SQRLUpdaterState;
|
||||
|
||||
// The domain for errors originating within SQRLUpdater.
|
||||
extern NSString * const SQRLUpdaterErrorDomain;
|
||||
|
||||
@@ -69,11 +55,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
|
||||
// If an update is available, it will be sent on `updates` once downloaded.
|
||||
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
|
||||
|
||||
// The current state of the manager.
|
||||
//
|
||||
// This property is KVO-compliant.
|
||||
@property (atomic, readonly) SQRLUpdaterState state;
|
||||
|
||||
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
|
||||
// update is available.
|
||||
//
|
||||
|
||||
+47
-29
@@ -15,9 +15,9 @@
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLUpdate.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@@ -38,8 +38,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
|
||||
@interface SQRLUpdater ()
|
||||
|
||||
@property (atomic, readwrite) SQRLUpdaterState state;
|
||||
|
||||
// The code signature for the running application, used to check updates before
|
||||
// sending them to ShipIt.
|
||||
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
|
||||
@@ -126,6 +124,16 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
// Returns a signal which completes or errors on a background thread.
|
||||
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update;
|
||||
|
||||
// Verifies that an existing state is innocuous, and therefore safe to
|
||||
// overwrite.
|
||||
//
|
||||
// This won't be the case if, for example, an update is currently being
|
||||
// installed.
|
||||
//
|
||||
// Returns a signal which sends `existingState` then completes upon successful
|
||||
// validation, or errors otherwise.
|
||||
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLUpdater
|
||||
@@ -205,14 +213,12 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
// Prune old updates before the first update check.
|
||||
return [[[[[[[[self.prunedUpdateDirectories
|
||||
return [[[[[[[self.prunedUpdateDirectories
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error pruning old updates: %@", error);
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
then:^{
|
||||
self.state = SQRLUpdaterStateCheckingForUpdate;
|
||||
|
||||
return [NSURLConnection rac_sendAsynchronousRequest:request];
|
||||
}]
|
||||
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
|
||||
@@ -240,19 +246,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
return [self updateFromJSONData:data];
|
||||
}]
|
||||
flattenMap:^(SQRLUpdate *update) {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
self.state = SQRLUpdaterStateDownloadingUpdate;
|
||||
|
||||
return [self downloadAndPrepareUpdate:update];
|
||||
}]
|
||||
doCompleted:^{
|
||||
self.state = SQRLUpdaterStateAwaitingRelaunch;
|
||||
}];
|
||||
}]
|
||||
finally:^{
|
||||
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return;
|
||||
self.state = SQRLUpdaterStateIdle;
|
||||
return [self downloadAndPrepareUpdate:update];
|
||||
}]
|
||||
deliverOn:RACScheduler.mainThreadScheduler];
|
||||
}];
|
||||
@@ -522,14 +516,37 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
setNameWithFormat:@"%@ -verifyAndPrepareUpdate: %@ fromBundle: %@", self, update, updateBundle];
|
||||
}
|
||||
|
||||
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
|
||||
// If this happens, shit is crazy, because it implies that an
|
||||
// update is being installed over us right now.
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:existingState];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -validateExistingState: %@", self, existingState];
|
||||
}
|
||||
|
||||
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update {
|
||||
NSParameterAssert(update != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO];
|
||||
return [request writeUsingURL:self.shipItStateURL];
|
||||
return [[[[[[[SQRLShipItState
|
||||
readUsingURL:self.shipItStateURL]
|
||||
catchTo:[RACSignal empty]]
|
||||
flattenMap:^(SQRLShipItState *existingState) {
|
||||
return [self validateExistingState:existingState];
|
||||
}]
|
||||
then:^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
|
||||
return [state writeUsingURL:self.shipItStateURL];
|
||||
}]
|
||||
then:^{
|
||||
return self.shipItLauncher;
|
||||
@@ -539,13 +556,14 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
}
|
||||
|
||||
- (RACSignal *)relaunchToInstallUpdate {
|
||||
return [[[[[[[[SQRLShipItRequest
|
||||
return [[[[[[[[SQRLShipItState
|
||||
readUsingURL:self.shipItStateURL]
|
||||
map:^(SQRLShipItRequest *request) {
|
||||
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES];
|
||||
flattenMap:^(SQRLShipItState *existingState) {
|
||||
return [self validateExistingState:existingState];
|
||||
}]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [[request
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
state.relaunchAfterInstallation = YES;
|
||||
return [[state
|
||||
writeUsingURL:self.shipItStateURL]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
|
||||
}]
|
||||
|
||||
+96
-105
@@ -11,11 +11,11 @@
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import "RACSignal+SQRLTransactionExtensions.h"
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLTerminationListener.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
// The maximum number of times ShipIt should run the same installation state, in
|
||||
// an attempt to update.
|
||||
@@ -27,116 +27,19 @@ static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
|
||||
// The domain for errors generated here.
|
||||
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
|
||||
|
||||
static NSUInteger installationAttempts(NSString *applicationIdentifier) {
|
||||
return CFPreferencesGetAppIntegerValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFStringRef)applicationIdentifier, NULL);
|
||||
}
|
||||
|
||||
static BOOL setInstallationAttempts(NSString *applicationIdentifier, NSUInteger attempts) {
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(attempts), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
}
|
||||
|
||||
static BOOL clearInstallationAttempts(NSString *applicationIdentifier) {
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, NULL, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
}
|
||||
|
||||
// Waits for all instances of the target application (as described in the
|
||||
// `request`) to exit, then sends completed.
|
||||
static RACSignal *waitForTerminationIfNecessary(SQRLShipItRequest *request) {
|
||||
// `state`) to exit, then sends completed.
|
||||
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
if (request.bundleIdentifier == nil) return [RACSignal empty];
|
||||
if (state.bundleIdentifier == nil) return [RACSignal empty];
|
||||
|
||||
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier];
|
||||
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
|
||||
return [listener waitForTermination];
|
||||
}]
|
||||
setNameWithFormat:@"waitForTerminationIfNecessary"];
|
||||
}
|
||||
|
||||
static void installRequest(RACSignal *readRequestSignal, SQRLDirectoryManager *directoryManager) {
|
||||
NSString *applicationIdentifier = directoryManager.applicationIdentifier;
|
||||
|
||||
[[[[[readRequestSignal
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return waitForTerminationIfNecessary(request);
|
||||
}]
|
||||
ignoreValues]
|
||||
concat:readRequestSignal]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
|
||||
|
||||
NSUInteger attempt = installationAttempts(applicationIdentifier) + 1;
|
||||
setInstallationAttempts(applicationIdentifier, attempt);
|
||||
|
||||
RACSignal *action;
|
||||
if (attempt > SQRLShipItMaximumInstallationAttempts) {
|
||||
action = [[[installer.abortInstallationCommand
|
||||
execute:request]
|
||||
initially:^{
|
||||
NSLog(@"Too many attempts to install, aborting update");
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error aborting installation: %@", error);
|
||||
|
||||
// Exit successfully so launchd doesn't restart us
|
||||
// again.
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
} else {
|
||||
action = [[[[installer.installUpdateCommand
|
||||
execute:request]
|
||||
initially:^{
|
||||
BOOL freshInstall = (attempt == 1);
|
||||
if (freshInstall) {
|
||||
NSLog(@"Beginning installation");
|
||||
} else {
|
||||
NSLog(@"Resuming installation attempt %i", (int)attempt);
|
||||
}
|
||||
}]
|
||||
doCompleted:^{
|
||||
NSLog(@"Installation completed successfully");
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path];
|
||||
}
|
||||
|
||||
// Clear the installation attempts for a successful abort or
|
||||
// install.
|
||||
action = [action doCompleted:^{
|
||||
clearInstallationAttempts(applicationIdentifier);
|
||||
}];
|
||||
|
||||
if (request.launchAfterInstallation) {
|
||||
// Launch regardless of whether installation succeeds or fails.
|
||||
action = [[action
|
||||
deliverOn:RACScheduler.mainThreadScheduler]
|
||||
finally:^{
|
||||
NSURL *bundleURL = request.targetBundleURL;
|
||||
if (bundleURL == nil) {
|
||||
NSLog(@"Missing target bundle URL, cannot launch application");
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
|
||||
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"Application launched at %@", bundleURL);
|
||||
}];
|
||||
}
|
||||
|
||||
return action;
|
||||
}]
|
||||
subscribeError:^(NSError *error) {
|
||||
NSLog(@"Installation error: %@", error);
|
||||
exit(EXIT_FAILURE);
|
||||
} completed:^{
|
||||
exit(EXIT_SUCCESS);
|
||||
}];
|
||||
}
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
atexit_b(^{
|
||||
@@ -148,13 +51,101 @@ int main(int argc, const char * argv[]) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char const *jobLabel = argv[1];
|
||||
const char *jobLabel = argv[1];
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
|
||||
RACSignal *stateLocation = directoryManager.shipItStateURL;
|
||||
|
||||
installRequest([SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL], directoryManager);
|
||||
[[[[[[SQRLShipItState
|
||||
readUsingURL:stateLocation]
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
return waitForTerminationIfNecessary(state);
|
||||
}]
|
||||
then:^{
|
||||
// Read the latest state, in case it was modified by the
|
||||
// controlling application in the meantime.
|
||||
return [SQRLShipItState readUsingURL:stateLocation];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error reading saved installer state: %@", error);
|
||||
|
||||
// Exit successfully so launchd doesn't restart us again.
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
BOOL freshInstall = (state.installerState == SQRLInstallerStateNothingToDo);
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:directoryManager];
|
||||
|
||||
NSUInteger attempt = (freshInstall ? 1 : state.installationStateAttempt + 1);
|
||||
RACSignal *action;
|
||||
|
||||
if (attempt > SQRLShipItMaximumInstallationAttempts) {
|
||||
action = [[[installer.abortInstallationCommand
|
||||
execute:state]
|
||||
initially:^{
|
||||
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error aborting installation: %@", error);
|
||||
|
||||
// Exit successfully so launchd doesn't restart us again.
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
} else {
|
||||
action = [[[[[state
|
||||
writeUsingURL:stateLocation]
|
||||
initially:^{
|
||||
if (freshInstall) {
|
||||
NSLog(@"Beginning installation");
|
||||
state.installerState = SQRLInstallerStateClearingQuarantine;
|
||||
} else {
|
||||
NSLog(@"Resuming installation from state %i", (int)state.installerState);
|
||||
}
|
||||
|
||||
state.installationStateAttempt = attempt;
|
||||
}]
|
||||
then:^{
|
||||
return [installer.installUpdateCommand execute:state];
|
||||
}]
|
||||
doCompleted:^{
|
||||
NSLog(@"Installation completed successfully");
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
|
||||
}
|
||||
|
||||
if (state.relaunchAfterInstallation) {
|
||||
// Relaunch regardless of whether installation succeeds or
|
||||
// fails.
|
||||
action = [[action
|
||||
deliverOn:RACScheduler.mainThreadScheduler]
|
||||
finally:^{
|
||||
NSURL *bundleURL = state.targetBundleURL;
|
||||
if (bundleURL == nil) {
|
||||
NSLog(@"Missing target bundle URL, cannot relaunch application");
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
|
||||
NSLog(@"Could not relaunch application at %@: %@", bundleURL, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"Application relaunched at %@", bundleURL);
|
||||
}];
|
||||
}
|
||||
|
||||
return action;
|
||||
}]
|
||||
subscribeError:^(NSError *error) {
|
||||
NSLog(@"Installation error: %@", error);
|
||||
exit(EXIT_FAILURE);
|
||||
} completed:^{
|
||||
exit(EXIT_SUCCESS);
|
||||
}];
|
||||
|
||||
dispatch_main();
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -30,11 +30,11 @@ it(@"should verify a valid bundle", ^{
|
||||
|
||||
it(@"should fail to verify with different code signing requirements", ^{
|
||||
NSError *error = nil;
|
||||
SQRLCodeSignature *signature = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
expect(signature).notTo.beNil();
|
||||
SQRLCodeSignature *verifier = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
expect(verifier).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
BOOL success = [[verifier verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(error).notTo.beNil();
|
||||
});
|
||||
@@ -9,43 +9,22 @@
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
#import "SQRLShipItState.h"
|
||||
|
||||
SpecBegin(SQRLInstaller)
|
||||
|
||||
mode_t (^modeOfURL)(NSURL *) = ^ mode_t (NSURL *fileURL) {
|
||||
NSFileSecurity *fileSecurity = nil;
|
||||
BOOL success = [fileURL getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:NULL];
|
||||
expect(success).to.beTruthy();
|
||||
expect(fileSecurity).notTo.beNil();
|
||||
|
||||
__block mode_t mode;
|
||||
expect(CFFileSecurityGetMode((__bridge CFFileSecurityRef)fileSecurity, &mode)).to.beTruthy();
|
||||
|
||||
return mode & (S_IRWXU | S_IRWXG | S_IRWXO);
|
||||
};
|
||||
|
||||
__block NSURL *updateURL;
|
||||
|
||||
beforeEach(^{
|
||||
updateURL = [self createTestApplicationUpdate];
|
||||
});
|
||||
|
||||
it(@"should install an update using ShipIt", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
it(@"should install an update", ^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should install an update in process", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
[self installWithRequest:request remote:NO];
|
||||
[self launchShipIt];
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
@@ -55,9 +34,11 @@ it(@"should install an update and relaunch", ^{
|
||||
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier];
|
||||
expect(apps.count).to.equal(0);
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.relaunchAfterInstallation = YES;
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count).will.equal(1);
|
||||
@@ -67,9 +48,10 @@ it(@"should install an update from another volume", ^{
|
||||
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication 2.1" fromDirectory:updateURL.URLByDeletingLastPathComponent];
|
||||
updateURL = [diskImageURL URLByAppendingPathComponent:updateURL.lastPathComponent];
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
@@ -78,80 +60,70 @@ it(@"should install an update to another volume", ^{
|
||||
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication" fromDirectory:self.testApplicationURL.URLByDeletingLastPathComponent];
|
||||
NSURL *targetURL = [diskImageURL URLByAppendingPathComponent:self.testApplicationURL.lastPathComponent];
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
NSURL *plistURL = [targetURL URLByAppendingPathComponent:@"Contents/Info.plist"];
|
||||
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"with backup restoration", ^{
|
||||
__block NSURL *targetURL;
|
||||
it(@"should install an update in process", ^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.installerState = SQRLInstallerStateClearingQuarantine;
|
||||
|
||||
__block SQRLShipItRequest *request;
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
|
||||
expect(installer).notTo.beNil();
|
||||
|
||||
beforeEach(^{
|
||||
targetURL = self.testApplicationURL;
|
||||
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
NSURL *copiedTargetURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication Target.app"];
|
||||
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:copiedTargetURL error:NULL]).to.beTruthy();
|
||||
|
||||
SQRLCodeSignature *codeSignature = self.testApplicationSignature;
|
||||
|
||||
SQRLInstallerOwnedBundle *ownedBundle = [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:targetURL temporaryURL:copiedTargetURL codeSignature:codeSignature];
|
||||
NSData *ownedBundleArchive = [NSKeyedArchiver archivedDataWithRootObject:ownedBundle];
|
||||
expect(ownedBundleArchive).notTo.beNil();
|
||||
|
||||
// Set up ShipIt's preferences like it paused in the middle of an
|
||||
// installation.
|
||||
NSString *applicationIdentifier = self.shipItDirectoryManager.applicationIdentifier;
|
||||
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(4), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFDataRef)ownedBundleArchive, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
|
||||
BOOL synchronized = CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
expect(synchronized).to.beTruthy();
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
__block NSError *error;
|
||||
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should not install an update after too many attempts", ^{
|
||||
[self installWithRequest:request remote:YES];
|
||||
});
|
||||
|
||||
it(@"should relaunch even after failing to install an update", ^{
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
|
||||
});
|
||||
NSError *installError = nil;
|
||||
BOOL install = [[installer.installUpdateCommand execute:state] asynchronouslyWaitUntilCompleted:&installError];
|
||||
expect(install).to.beTruthy();
|
||||
expect(installError).to.beNil();
|
||||
});
|
||||
|
||||
it(@"should disallow writing the updated application except by the owner", ^{
|
||||
NSString *command = [NSString stringWithFormat:@"chmod -R 0777 '%@'", updateURL.path];
|
||||
expect(system(command.UTF8String)).to.equal(0);
|
||||
it(@"should not install an update after too many attempts", ^{
|
||||
NSURL *targetURL = self.testApplicationURL;
|
||||
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
|
||||
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
|
||||
|
||||
expect(modeOfURL(updateURL)).to.equal(0777);
|
||||
expect(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0777);
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.backupBundleURL = backupURL;
|
||||
state.installerState = SQRLInstallerStateInstalling;
|
||||
state.installationStateAttempt = 4;
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
[self launchShipIt];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
// No update should've been installed, and the application should be
|
||||
// restored from the backup.
|
||||
__block NSError *error = nil;
|
||||
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
expect(modeOfURL(self.testApplicationURL)).to.equal(0755);
|
||||
expect(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0755);
|
||||
it(@"should relaunch even after failing to install an update", ^{
|
||||
NSURL *targetURL = self.testApplicationURL;
|
||||
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
|
||||
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
|
||||
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.backupBundleURL = backupURL;
|
||||
state.installerState = SQRLInstallerStateInstalling;
|
||||
state.installationStateAttempt = 4;
|
||||
state.relaunchAfterInstallation = YES;
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self launchShipIt];
|
||||
|
||||
__block NSError *error = nil;
|
||||
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"signal handling", ^{
|
||||
@@ -162,9 +134,13 @@ describe(@"signal handling", ^{
|
||||
// accessing the property.
|
||||
targetURL = self.testApplicationURL;
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
// Wait until ShipIt has transitioned by at least one state.
|
||||
expect([[[SQRLShipItState readUsingURL:self.shipItDirectoryManager.shipItStateURL] asynchronousFirstOrDefault:nil success:NULL error:NULL] installerState]).willNot.equal(SQRLInstallerStateNothingToDo);
|
||||
|
||||
// Apply a random delay before sending the termination signal, to
|
||||
// fuzz out race conditions.
|
||||
@@ -178,7 +154,7 @@ describe(@"signal handling", ^{
|
||||
Expecta.asynchronousTestTimeout = 5;
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
|
||||
NSError *error;
|
||||
NSError *error = nil;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
@@ -202,11 +178,8 @@ describe(@"signal handling", ^{
|
||||
|
||||
it(@"should handle SIGKILL", ^{
|
||||
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
|
||||
// a couple times to really test resumption.
|
||||
//
|
||||
// Two SIGKILL signals means that it'll be launched three times (which
|
||||
// matches the maximum number of attempts per state).
|
||||
for (int i = 0; i < 2; i++) {
|
||||
// a few times to really test resumption.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
system("killall -KILL ShipIt");
|
||||
|
||||
// Wait at least for the launchd throttle interval.
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
//
|
||||
// SQRLShipItStateSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-09.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
SpecBegin(SQRLShipItRequest)
|
||||
|
||||
__block SQRLDirectoryManager *directoryManager;
|
||||
__block SQRLShipItRequest *request;
|
||||
|
||||
beforeEach(^{
|
||||
directoryManager = SQRLDirectoryManager.currentApplicationManager;
|
||||
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
expect(request).notTo.beNil();
|
||||
|
||||
expect(request.targetBundleURL).to.equal(self.testApplicationURL);
|
||||
expect(request.updateBundleURL).to.equal(updateURL);
|
||||
expect(request.bundleIdentifier).to.beNil();
|
||||
expect(request.launchAfterInstallation).to.beFalsy();
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
NSURL *stateURL = [[directoryManager shipItStateURL] first];
|
||||
expect(stateURL).notTo.beNil();
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
});
|
||||
|
||||
it(@"should copy", ^{
|
||||
SQRLShipItRequest *requestCopy = [request copy];
|
||||
expect(requestCopy).to.equal(request);
|
||||
expect(requestCopy).notTo.beIdenticalTo(request);
|
||||
});
|
||||
|
||||
it(@"should fail to read state when no file exists yet", ^{
|
||||
NSError *error;
|
||||
BOOL success = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(error).notTo.beNil();
|
||||
});
|
||||
|
||||
it(@"should write and read state to disk", ^{
|
||||
NSError *error;
|
||||
BOOL success = [[request writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(readRequest).to.equal(request);
|
||||
});
|
||||
|
||||
it(@"should fail gracefully with archives encoding a different class", ^{
|
||||
NSURL *archiveLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"archive"];
|
||||
|
||||
NSError *error;
|
||||
BOOL write = [[NSKeyedArchiver archivedDataWithRootObject:@"rogue object"] writeToURL:archiveLocation atomically:YES];
|
||||
expect(write).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
BOOL success = NO;
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest readUsingURL:[RACSignal return:archiveLocation]] firstOrDefault:nil success:&success error:&error];
|
||||
expect(request).to.beNil();
|
||||
expect(success).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLShipItRequestErrorDomain);
|
||||
expect(error.code).to.equal(SQRLShipItRequestErrorUnarchiving);
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// SQRLShipItStateSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-09.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItState.h"
|
||||
|
||||
SpecBegin(SQRLShipItState)
|
||||
|
||||
__block SQRLDirectoryManager *directoryManager;
|
||||
__block SQRLShipItState *state;
|
||||
|
||||
beforeEach(^{
|
||||
directoryManager = SQRLDirectoryManager.currentApplicationManager;
|
||||
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect(state).notTo.beNil();
|
||||
|
||||
expect(state.targetBundleURL).to.equal(self.testApplicationURL);
|
||||
expect(state.updateBundleURL).to.equal(updateURL);
|
||||
expect(state.bundleIdentifier).to.beNil();
|
||||
expect(state.codeSignature).to.equal(self.testApplicationSignature);
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
NSURL *stateURL = [[directoryManager shipItStateURL] firstOrDefault:nil success:NULL error:NULL];
|
||||
expect(stateURL).notTo.beNil();
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
});
|
||||
|
||||
it(@"should copy", ^{
|
||||
SQRLShipItState *stateCopy = [state copy];
|
||||
expect(stateCopy).to.equal(state);
|
||||
expect(stateCopy).notTo.beIdenticalTo(state);
|
||||
});
|
||||
|
||||
it(@"should fail to read state when no file exists yet", ^{
|
||||
NSError *error = nil;
|
||||
BOOL success = [[SQRLShipItState readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(error).notTo.beNil();
|
||||
});
|
||||
|
||||
it(@"should write and read state", ^{
|
||||
NSError *error = nil;
|
||||
BOOL success = [[state writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
SQRLShipItState *readState = [[SQRLShipItState readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(readState).to.equal(state);
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -18,9 +18,8 @@ extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
|
||||
@class SQRLCodeSignature;
|
||||
@class SQRLDirectoryManager;
|
||||
@class SQRLShipItRequest;
|
||||
|
||||
@interface SQRLTestCase : SPTXCTestCase
|
||||
@interface SQRLTestCase : SPTSenTestCase
|
||||
|
||||
// A URL to a temporary directory tests can use.
|
||||
//
|
||||
@@ -46,6 +45,10 @@ extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
// A code signature with requirements from TestApplication.app.
|
||||
@property (nonatomic, strong, readonly) SQRLCodeSignature *testApplicationSignature;
|
||||
|
||||
// A serialized `SecRequirementRef` representing the requirements from
|
||||
// TestApplication.app.
|
||||
@property (nonatomic, copy, readonly) NSData *testApplicationCodeSigningRequirementData;
|
||||
|
||||
// A directory manager for finding URLs that apply to ShipIt.
|
||||
@property (nonatomic, strong, readonly) SQRLDirectoryManager *shipItDirectoryManager;
|
||||
|
||||
@@ -71,8 +74,8 @@ extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
// deleted at the end of the example.
|
||||
- (NSURL *)createTestApplicationUpdate;
|
||||
|
||||
// Runs the installer in process or submits ShipIt's launchd job to start it up.
|
||||
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
|
||||
// Submits ShipIt's launchd job to start it up.
|
||||
- (void)launchShipIt;
|
||||
|
||||
// Creates a disk image, then mounts it.
|
||||
//
|
||||
|
||||
@@ -7,13 +7,9 @@
|
||||
//
|
||||
|
||||
#import "SQRLTestCase.h"
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLTestHelper.h"
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
|
||||
#pragma clang diagnostic push
|
||||
@@ -114,6 +110,20 @@ static void SQRLSignalHandler(int sig) {
|
||||
Expecta.asynchronousTestTimeout = 3;
|
||||
}
|
||||
|
||||
- (void)SPT_setUp {
|
||||
_exampleCleanupBlocks = [[NSMutableArray alloc] init];
|
||||
}
|
||||
|
||||
- (void)SPT_tearDown {
|
||||
NSArray *cleanupBlocks = [self.exampleCleanupBlocks copy];
|
||||
_exampleCleanupBlocks = nil;
|
||||
|
||||
// Enumerate backwards, so later resources are cleaned up first.
|
||||
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
|
||||
block();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[super tearDown];
|
||||
|
||||
@@ -121,7 +131,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
}
|
||||
|
||||
- (void)addCleanupBlock:(dispatch_block_t)block {
|
||||
[SQRLTestHelper addCleanupBlock:block];
|
||||
[self.exampleCleanupBlocks addObject:[block copy]];
|
||||
}
|
||||
|
||||
#pragma mark Logging
|
||||
@@ -151,7 +161,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:_baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
|
||||
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:_baseTemporaryDirectoryURL error:NULL];
|
||||
@@ -167,7 +177,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
|
||||
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
|
||||
|
||||
return temporaryDirectoryURL;
|
||||
}
|
||||
@@ -178,11 +188,11 @@ static void SQRLSignalHandler(int sig) {
|
||||
NSURL *fixtureURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app" isDirectory:YES];
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:fixtureURL.path]) {
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:fixtureURL error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
|
||||
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
|
||||
|
||||
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
|
||||
[[NSData data] writeToURL:testAppLog atomically:YES];
|
||||
@@ -190,7 +200,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSequence return:testAppLog.path]];
|
||||
[readTestApp launch];
|
||||
|
||||
XCTAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
|
||||
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[readTestApp terminate];
|
||||
@@ -203,7 +213,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
- (NSBundle *)testApplicationBundle {
|
||||
NSURL *fixtureURL = self.testApplicationURL;
|
||||
NSBundle *bundle = [NSBundle bundleWithURL:fixtureURL];
|
||||
XCTAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
|
||||
STAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
@@ -223,7 +233,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
NSError *error = nil;
|
||||
NSRunningApplication *app = [NSWorkspace.sharedWorkspace launchApplicationAtURL:self.testApplicationURL options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchAndHide configuration:configuration error:&error];
|
||||
XCTAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
|
||||
STAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
if (!app.terminated) {
|
||||
@@ -244,11 +254,11 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
- (NSURL *)createTestApplicationUpdate {
|
||||
NSURL *originalURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication 2.1" withExtension:@"app"];
|
||||
XCTAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
|
||||
STAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
NSURL *updateParentURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:self.baseTemporaryDirectoryURL create:YES error:&error];
|
||||
XCTAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
|
||||
STAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:updateParentURL error:NULL];
|
||||
@@ -256,18 +266,18 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
NSURL *updateURL = [updateParentURL URLByAppendingPathComponent:originalURL.lastPathComponent isDirectory:YES];
|
||||
BOOL success = [NSFileManager.defaultManager copyItemAtURL:originalURL toURL:updateURL error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
|
||||
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
|
||||
|
||||
return updateURL;
|
||||
}
|
||||
|
||||
- (id)performWithTestApplicationRequirement:(id (^)(SecRequirementRef requirement))block {
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
SecStaticCodeRef staticCode = NULL;
|
||||
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
|
||||
XCTAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
|
||||
STAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
|
||||
|
||||
@onExit {
|
||||
if (staticCode != NULL) CFRelease(staticCode);
|
||||
@@ -275,7 +285,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
SecRequirementRef requirement = NULL;
|
||||
status = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &requirement);
|
||||
XCTAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
|
||||
STAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
|
||||
|
||||
@onExit {
|
||||
if (requirement != NULL) CFRelease(requirement);
|
||||
@@ -285,57 +295,48 @@ static void SQRLSignalHandler(int sig) {
|
||||
}
|
||||
|
||||
- (SQRLCodeSignature *)testApplicationSignature {
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
|
||||
return [[SQRLCodeSignature alloc] initWithRequirement:requirement];
|
||||
}];
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
SQRLCodeSignature *signature = [SQRLCodeSignature signatureWithBundle:bundleURL error:&error];
|
||||
XCTAssertNotNil(signature, @"Error getting signature for bundle at %@: %@", bundleURL, error);
|
||||
- (NSData *)testApplicationCodeSigningRequirementData {
|
||||
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
|
||||
CFDataRef data = NULL;
|
||||
OSStatus status = SecRequirementCopyData(requirement, kSecCSDefaultFlags, &data);
|
||||
STAssertTrue(status == noErr, @"Error copying data for requirement %@", requirement);
|
||||
|
||||
return signature;
|
||||
return CFBridgingRelease(data);
|
||||
}];
|
||||
}
|
||||
|
||||
- (SQRLDirectoryManager *)shipItDirectoryManager {
|
||||
NSString *identifier = SQRLShipItLauncher.shipItJobLabel;
|
||||
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:identifier];
|
||||
XCTAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
|
||||
STAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote {
|
||||
if (remote) {
|
||||
expect([[request writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
- (void)launchShipIt {
|
||||
NSError *error = nil;
|
||||
STAssertTrue([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error], @"Could not launch ShipIt: %@", error);
|
||||
|
||||
__block NSError *error = nil;
|
||||
expect([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error]).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
[self addCleanupBlock:^{
|
||||
// Remove ShipIt's launchd job so it doesn't relaunch itself.
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
// Remove ShipIt's launchd job so it doesn't relaunch itself.
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
|
||||
|
||||
NSError *lookupError;
|
||||
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
|
||||
expect(stateURL).notTo.beNil();
|
||||
expect(lookupError).to.beNil();
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
}];
|
||||
} else {
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:self.shipItDirectoryManager.applicationIdentifier];
|
||||
expect(installer).notTo.beNil();
|
||||
|
||||
NSError *installedError = nil;
|
||||
BOOL installed = [[installer.installUpdateCommand execute:request] asynchronouslyWaitUntilCompleted:&installedError];
|
||||
expect(installed).to.beTruthy();
|
||||
expect(installedError).to.beNil();
|
||||
}
|
||||
NSError *lookupError = nil;
|
||||
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
|
||||
STAssertNotNil(stateURL, @"Could not find state URL from %@: %@", self.shipItDirectoryManager, lookupError);
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL {
|
||||
NSURL *destinationURL = [self.baseTemporaryDirectoryURL URLByAppendingPathComponent:name];
|
||||
XCTAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
|
||||
STAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
|
||||
|
||||
NSString *createInvocation;
|
||||
if (directoryURL == nil) {
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// SQRLTestHelper.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Matt Diephouse on 7/7/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// A test helper class that will run cleanup blocks after each test finishes.
|
||||
@interface SQRLTestHelper : NSObject
|
||||
|
||||
/// Adds a cleanup block that will run after the next test completes.
|
||||
+ (void)addCleanupBlock:(dispatch_block_t)block;
|
||||
|
||||
@end
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// SQRLTestHelper.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Matt Diephouse on 7/7/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLTestHelper.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation SQRLTestHelper
|
||||
|
||||
#pragma mark - Specta Methods
|
||||
|
||||
+ (void)afterEach {
|
||||
// Enumerate backwards, so later resources are cleaned up first.
|
||||
for (dispatch_block_t block in self.cleanupBlocks.reverseObjectEnumerator) {
|
||||
block();
|
||||
}
|
||||
[self.cleanupBlocks removeAllObjects];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
+ (void)addCleanupBlock:(dispatch_block_t)block {
|
||||
[self.cleanupBlocks addObject:[block copy]];
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
+ (NSMutableArray *)cleanupBlocks {
|
||||
NSMutableArray *blocks = objc_getAssociatedObject(self, "sqrl_cleanupBlocks");
|
||||
if (blocks == nil) {
|
||||
blocks = [NSMutableArray array];
|
||||
objc_setAssociatedObject(self, "sqrl_cleanupBlocks", blocks, OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import "OHHTTPStubs/OHHTTPStubs.h"
|
||||
#import "TestAppConstants.h"
|
||||
|
||||
SpecBegin(SQRLUpdater)
|
||||
|
||||
@@ -178,7 +177,13 @@ describe(@"updating", ^{
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
|
||||
expect([updateDirectoryURLs toArray]).will.equal(@[]);
|
||||
// Also test that this is the only update directory (any others
|
||||
// should've been removed).
|
||||
NSArray *directoryURLs = [updateDirectoryURLs toArray];
|
||||
expect(directoryURLs.count).to.equal(1);
|
||||
|
||||
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
|
||||
expect(contents).to.equal(@[]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -227,62 +232,4 @@ describe(@"response handling", ^{
|
||||
});
|
||||
});
|
||||
|
||||
static RACSignal * (^stateNotificationListener)(void) = ^ {
|
||||
return [[[NSDistributedNotificationCenter.defaultCenter
|
||||
rac_addObserverForName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil]
|
||||
map:^(NSNotification *notification) {
|
||||
return notification.userInfo[SQRLTestAppUpdaterStateKey];
|
||||
}]
|
||||
setNameWithFormat:@"stateNotificationListener"];
|
||||
};
|
||||
|
||||
describe(@"state", ^{
|
||||
it(@"should transition through idle, checking and idle, when there is no update", ^{
|
||||
NSMutableArray *states = [NSMutableArray array];
|
||||
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
|
||||
[states addObject:state];
|
||||
}];
|
||||
|
||||
NSRunningApplication *testApplication = launchWithEnvironment(nil);
|
||||
|
||||
NSArray *expectedStates = @[
|
||||
@(SQRLUpdaterStateIdle),
|
||||
@(SQRLUpdaterStateCheckingForUpdate),
|
||||
@(SQRLUpdaterStateIdle),
|
||||
];
|
||||
expect(states).will.equal(expectedStates);
|
||||
|
||||
expect(testApplication.terminated).will.beTruthy();
|
||||
});
|
||||
|
||||
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
|
||||
NSMutableArray *states = [NSMutableArray array];
|
||||
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
|
||||
[states addObject:state];
|
||||
}];
|
||||
|
||||
NSError *error;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate([self createTestApplicationUpdate]),
|
||||
@"final": @YES,
|
||||
} error:&error];
|
||||
expect(update).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *testApplication = launchWithEnvironment(nil);
|
||||
|
||||
NSArray *expectedStates = @[
|
||||
@(SQRLUpdaterStateIdle),
|
||||
@(SQRLUpdaterStateCheckingForUpdate),
|
||||
@(SQRLUpdaterStateDownloadingUpdate),
|
||||
@(SQRLUpdaterStateAwaitingRelaunch),
|
||||
];
|
||||
expect(states).will.equal(expectedStates);
|
||||
|
||||
expect(testApplication.terminated).will.beTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
#import <SenTestingKit/SenTestingKit.h>
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
@@ -21,8 +21,5 @@
|
||||
#define EXP_SHORTHAND
|
||||
#import "Expecta.h"
|
||||
|
||||
// Expecta #defines startWith as beginWith, which messes up -startWith: :-(
|
||||
#undef startWith
|
||||
|
||||
#import "SQRLTestCase.h"
|
||||
#endif
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// TestAppConstants.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 05/02/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// This notification is posted to the distributed notification center when a
|
||||
// change to the `SQRLUpdater.state` property is observed.
|
||||
//
|
||||
// It is posted once on launch with the initial idle state, and then for any
|
||||
// transition thereafter.
|
||||
//
|
||||
// Yhe user info dictionary includes the new state value under the
|
||||
// `SQRLTestAppUpdaterStateKey` key.
|
||||
extern NSString * const SQRLTestAppUpdaterStateTransitionNotificationName;
|
||||
|
||||
// The state of the updater, an NSNumber.
|
||||
extern NSString * const SQRLTestAppUpdaterStateKey;
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// TestAppConstants.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 05/02/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TestAppConstants.h"
|
||||
|
||||
NSString * const SQRLTestAppUpdaterStateTransitionNotificationName = @"com.github.Squirrel.TestApplication.state-changed";
|
||||
|
||||
NSString * const SQRLTestAppUpdaterStateKey = @"state";
|
||||
@@ -12,7 +12,6 @@
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "TestAppConstants.h"
|
||||
|
||||
@interface TestAppDelegate ()
|
||||
|
||||
@@ -64,12 +63,6 @@
|
||||
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:request];
|
||||
self.updater.updateClass = SQRLTestUpdate.class;
|
||||
|
||||
[RACObserve(self.updater, state) subscribeNext:^(NSNumber *state) {
|
||||
NSLog(@"State transition: %@", state);
|
||||
|
||||
[NSDistributedNotificationCenter.defaultCenter postNotificationName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil userInfo:@{ SQRLTestAppUpdaterStateKey: state }];
|
||||
}];
|
||||
|
||||
__block NSUInteger updateCheckCount = 1;
|
||||
|
||||
[[[[[[[[[[RACSignal
|
||||
|
||||
Reference in New Issue
Block a user