Author SHA1 Message Date
Keith Duncan 9b12e8faa7 Move all the error codes into one domain 2013-10-30 13:03:04 +00:00
Keith Duncan e8c0e4748e Add Squirrel-Constants for SQRLErrorDomain 2013-10-30 12:38:11 +00:00
61 changed files with 2283 additions and 2844 deletions
+1 -4
View File
@@ -3,7 +3,4 @@
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/MantleFramework/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
url = https://github.com/github/Mantle.git
Submodule External/OHHTTPStubs deleted from 08776e99bc
+46 -112
View File
@@ -13,49 +13,21 @@ suitable update.
The update JSON Squirrel requests should be dynamically generated based on
criteria in the request, and whether an update is required. Squirrel relies
on server side support for determining whether an update is required, see
[Server Support](#server-support).
on server side support for determining whether an update is required, see [Server
Support](#server-support) below.
Squirrel's installer is also designed to be fault tolerant, and ensure that any
Squirrel’s installer is also designed to be fault tolerant, and ensure that any
updates installed are valid.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
# Adopting Squirrel
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)
and [Mantle](https://github.com/MantleFramework/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
includes the directory that Squirrel.framework, ReactiveCocoa.framework
and Mantle.framework are copied into.
# Configuration
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
Once Squirrel is added to your project, you need to configure and start it.
@@ -63,89 +35,37 @@ Once Squirrel is added to your project, you need to configure and start it.
#import <Squirrel/Squirrel.h>
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSURLComponents *components = [[NSURLComponents alloc] init];
components.scheme = @"http";
components.host = @"mycompany.com";
components.path = @"/myapp/latest";
NSString *bundleVersion = NSBundle.mainBundle.sqrl_bundleVersion;
components.query = [[NSString stringWithFormat:@"version=%@", bundleVersion] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:components.URL]];
// Check for updates every 4 hours.
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://mycompany.com/myapp/latest"]]];
[self.updater startAutomaticChecksWithInterval:/* 4 Hours */ 60 * 60 * 4];
[self.updater checkForUpdates];
}
```
Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
Squirrel will periodically request and automatically download any updates.
## Update Requests
Squirrel is indifferent to the request the client application provides for
update checking. `Accept: application/json` is added to the request headers
because Squirrel is responsible for parsing the response.
For the requirements imposed on the responses and the body format of an update
response see [Server Support](#server-support).
Your update request must *at least* include a version identifier so that the
server can determine whether an update for this specific version is required. It
may also include other identifying criteria such as operating system version or
username, to allow the server to deliver as fine grained an update as you
would like.
How you include the version identifier or other criteria is specific to the
server that you are requesting updates from. A common approach is to use query
parameters, [Configuration](#configuration) shows an example of this.
## Update Available Notifications
To know when an update is ready to be installed, you can subscribe to the
`updates` signal on `SQRLUpdater`:
Before your application terminates, it should tell Squirrel to install any
updates that it has downloaded:
```objc
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
NSLog(@"An update is ready to install: %@", downloadedUpdate);
}];
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
[self.updater installUpdateIfNeeded:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"Successfully installed an update");
} else {
NSLog(@"Failed to update: %@", error);
}
[sender replyToApplicationShouldTerminate:YES];
}];
return NSTerminateLater;
}
```
If you've been notified of an available update, and don't want to wait for it to
be installed automatically, you can terminate the app to begin the installation
process immediately.
# Update JSON Format
If you want to install a downloaded update and automatically relaunch afterward,
`SQRLUpdater` can do that:
```objc
[[self.updater relaunchToInstallUpdate] subscribeError:^(NSError *error) {
NSLog(@"Error preparing update: %@", error);
}];
```
# Server Support
Your server should determine whether an update is required based on the
[Update Request](#update-requests) your client issues.
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
[update JSON](#update-json-format) in the body. Squirrel **will** download and
install this update, even if the version of the update is the same as the
currently running version. To save redundantly downloading the same version
multiple times your server must not inform the client to update.
If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
## Update JSON Format
When an update is available, Squirrel expects the following schema in response
to the update request provided:
Squirrel requests the URL you provide with `Accept: application/json` and
expects the following schema in response:
```json
{
@@ -163,10 +83,24 @@ installing ZIP updates. If future update formats are supported their MIME type
will be added to the `Accept` header so that your server can return the
appropriate format.
"pub_date" if present must be formatted according to ISO 8601.
"pub_date" if present must be formatted according to ISO 8601
# Server Support
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
update JSON in the body. Squirrel **will** download and install this update,
even if the version of the update is the same as the currently running version.
To save redundantly downloading the same version multiple times your server must
inform the client not to update.
If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
# User Interface
Squirrel does not provide any GUI components for presenting updates. If you want
to indicate updates to the user, make sure to [listen for downloaded
updates](#update-notifications).
Squirrel does not provide an updates interface, if you want to display available
updates, subscribe to the `SQRLUpdaterUpdateAvailableNotification` notification.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
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"
@@ -15,8 +15,8 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "shipit-installer"
BlueprintName = "ShipIt Installer"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
@@ -28,16 +28,6 @@
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.xctest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
-22
View File
@@ -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>
-27
View File
@@ -1,27 +0,0 @@
//
// SQRLAuthorization.h
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Wraps an AuthorizationRef.
@interface SQRLAuthorization : NSObject
// Designated initializer.
//
// authorization - Must not be NULL, the returned object assumes ownership of
// the passed authorization.
//
// Returns an object which ties the `authorization` argument to the returned
// object's lifetime.
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
@end
-28
View File
@@ -1,28 +0,0 @@
//
// SQRLAuthorization.m
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLAuthorization.h"
@implementation SQRLAuthorization
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
NSParameterAssert(authorization != NULL);
self = [self init];
if (self == nil) return nil;
_authorization = authorization;
return self;
}
- (void)dealloc {
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
}
@end
+8 -30
View File
@@ -9,56 +9,34 @@
#import <Foundation/Foundation.h>
#import <Mantle/Mantle.h>
// The domain for errors originating within SQRLCodeSignature.
extern NSString * const SQRLCodeSignatureErrorDomain;
// The bundle did not pass codesign verification.
extern const NSInteger SQRLCodeSignatureErrorDidNotPass;
// A static code object could not be created for the target bundle or running
// code.
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
+36 -57
View File
@@ -10,23 +10,14 @@
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
const NSInteger SQRLCodeSignatureErrorDidNotPass = -1;
const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
#import "Squirrel-Constants.h"
@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 +33,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 +45,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 {
@@ -116,7 +95,7 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
NSString *failureReason = CFBridgingRelease(SecCopyErrorMessageString(result, NULL));
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return nil;
}
@@ -135,7 +114,7 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return nil;
}
-3
View File
@@ -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;
+8 -1
View File
@@ -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
@@ -23,7 +30,7 @@
// Should only fallback to when running under otest, where
// NSBundle.mainBundle doesn't return useful data.
if (identifier == nil) {
identifier = NSProcessInfo.processInfo.processName;
identifier = NSRunningApplication.currentApplication.localizedName;
}
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
-17
View File
@@ -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 -19
View File
@@ -8,10 +8,7 @@
#import <Foundation/Foundation.h>
// 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 +31,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 +57,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
+375 -338
View File
@@ -12,14 +12,13 @@
#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";
#import "Squirrel-Constants.h"
const NSInteger SQRLInstallerErrorBackupFailed = -1;
const NSInteger SQRLInstallerErrorReplacingTarget = -2;
@@ -28,58 +27,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`.
//
@@ -91,7 +117,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.
@@ -105,28 +131,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);
@@ -138,63 +155,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]];
@@ -202,162 +198,271 @@ 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:) },
{ .installerState = SQRLInstallerStateRelaunching, .selector = @selector(relaunchWithState:) },
};
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:SQRLErrorDomain 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];
}
- (RACSignal *)relaunchWithState:(SQRLShipItState *)state {
return [[[[RACSignal
defer:^{
if (state.relaunchAfterInstallation) {
return [self getRequiredKey:@keypath(state.targetBundleURL) fromState:state];
} else {
return [RACSignal empty];
}
}]
deliverOn:RACScheduler.mainThreadScheduler]
flattenMap:^(NSURL *bundleURL) {
NSError *error = nil;
if ([NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
return [RACSignal empty];
} else {
return [RACSignal error:error];
}
}]
setNameWithFormat:@"%@ -relaunch", self];
}
#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];
@@ -365,7 +470,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).
@@ -375,37 +480,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);
@@ -482,95 +608,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 {
@@ -581,7 +618,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
return [NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
}
- (NSError *)errorByAddingDescription:(NSString *)description code:(NSInteger)code toError:(NSError *)error {
@@ -590,7 +627,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
if (description != nil) userInfo[NSLocalizedDescriptionKey] = description;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:code userInfo:userInfo];
return [NSError errorWithDomain:SQRLErrorDomain code:code userInfo:userInfo];
}
@end
-37
View File
@@ -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
-23
View File
@@ -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
-81
View File
@@ -1,81 +0,0 @@
//
// SQRLShipItConnection.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
@class SQRLShipItRequest;
// The domain for errors originating within SQRLShipItConnection.
extern NSString * const SQRLShipItConnectionErrorDomain;
// The ShipIt service could not be started.
extern const NSInteger SQRLShipItConnectionErrorCouldNotStartService;
// Installing an update requires the coordination of multiple processes,
// `SQRLShipItConnection` is responsible for submitting the launchd jobs
// required to perform an installation request.
//
// The multiprocess approach to waiting for termination and installing is
// designed to keep uses of AppKit API (i.e. `NSRunningApplication`) out of the
// installer process - which may be running in the root bootstrap context - and
// put them in a process running in the user bootstrap context. These processes
// can then communicate using the file system as a message bus.
//
// Frustratingly although LaunchServices is daemon and root safe since 10.5, the
// LaunchServices API for querying the running applications isn't public
// (internally `NSRunningApplication` uses this LaunchServices private API,
// though of course this is subject to change and `NSRunningApplication` could
// also do other non-root safe things).
//
// When a user application running Squirrel wants to install an update, Squirrel
// submits two launchd jobs, one which will wait for application termination and
// write an empty file to the given location when the criteria are met, and the
// other which will wait for that file to appear and then perform the install as
// before. The "wait for termination" job is always submitted to the user
// domain, but the installer job is submitted to a domain based on whether the
// install location is writable by the current user.
//
// To perform the relaunch when the install is complete, it is safe to use the
// LaunchServices API from the installer process, the app will be launched in
// the user's GUI session.
//
// From <https://developer.apple.com/library/mac/technotes/tn2083/>
//
// > If the EUID of the calling process is zero, the application is launched in
// > the context of the currently active GUI login session. If there is no
// > currently active GUI login session (no one is logged in, or a logged in
// > user has fast user switched to the login window), the behavior is
// > unspecified (r. 5321293).
@interface SQRLShipItConnection : NSObject
// Returns the label for the ShipIt launchd job.
+ (NSString *)shipItInstallerJobLabel;
// Designated initialiser.
//
// privileged - Determines which launchd domain to launch the job in.
// If YES, ShipIt is launched in the root domain, otherwise it is
// launched in the current user’s domain.
//
// Returns an initialised connection which can be used to start an install.
- (instancetype)initWithRootPrivileges:(BOOL)rootPrivileges;
// Submits the request to the process network which will wait for termination
// and then install.
//
// After sending the request, the update will be attempted when the sending
// process terminates.
//
// request - The install parameters, target bundle, update bundle, whether to
// launch when install is complete etc. Must not be nil.
//
// Returns a signal which will complete, or error, on a background scheduler.
- (RACSignal *)sendRequest:(SQRLShipItRequest *)request;
@end
-203
View File
@@ -1,203 +0,0 @@
//
// SQRLShipItConnection.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLShipItConnection.h"
#import "EXTScope.h"
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
#import "SQRLShipItRequest.h"
NSString * const SQRLShipItConnectionErrorDomain = @"SQRLShipItConnectionErrorDomain";
const NSInteger SQRLShipItConnectionErrorCouldNotStartService = 1;
@interface SQRLShipItConnection ()
@property (readonly, nonatomic, assign) BOOL privileged;
@end
@implementation SQRLShipItConnection
+ (NSString *)shipItInstallerJobLabel {
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
}
+ (NSMutableDictionary *)jobDictionaryWithLabel:(NSString *)jobLabel executableName:(NSString *)executableName arguments:(NSArray *)arguments {
NSParameterAssert(jobLabel != nil);
NSParameterAssert(executableName != nil);
NSParameterAssert(arguments != nil);
NSMutableDictionary *jobDict = [NSMutableDictionary dictionary];
jobDict[@(LAUNCH_JOBKEY_LABEL)] = jobLabel;
jobDict[@(LAUNCH_JOBKEY_NICE)] = @(-1);
jobDict[@(LAUNCH_JOBKEY_ENABLETRANSACTIONS)] = @NO;
jobDict[@(LAUNCH_JOBKEY_THROTTLEINTERVAL)] = @2;
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
NSMutableArray *fullArguments = [NSMutableArray arrayWithObject:[squirrelBundle pathForResource:executableName ofType:nil]];
[fullArguments addObjectsFromArray:arguments];
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = fullArguments;
return jobDict;
}
+ (RACSignal *)shipItInstallerJobDictionary {
NSString *jobLabel = self.shipItInstallerJobLabel;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
return [directoryManager applicationSupportURL];
}]
map:^(NSURL *appSupportURL) {
NSMutableArray *arguments = [[NSMutableArray alloc] init];
// Pass in the service name so ShipIt knows how to broadcast itself.
[arguments addObject:jobLabel];
NSMutableDictionary *jobDict = [self jobDictionaryWithLabel:jobLabel executableName:@"shipit-installer" arguments:arguments];
jobDict[@(LAUNCH_JOBKEY_KEEPALIVE)] = @{
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
};
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
return jobDict;
}]
setNameWithFormat:@"+shipItInstallerJobDictionary"];
}
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
},
};
AuthorizationRights rights = {
.count = sizeof(rightItems) / sizeof(*rightItems),
.items = rightItems,
};
NSString *prompt = NSLocalizedString(@"An update is ready to install.", @"SQRLShipItConnection, launch shipit, authorization prompt");
NSString *iconName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
NSString *iconPath = (iconName == nil ? nil : [NSBundle.mainBundle pathForImageResource:iconName]);
AuthorizationItem environmentItems[] = {
{
.name = kAuthorizationEnvironmentPrompt,
.valueLength = strlen(prompt.UTF8String),
.value = (void *)prompt.UTF8String,
},
{
.name = kAuthorizationEnvironmentIcon,
.valueLength = (iconPath == nil ? 0 : strlen(iconPath.UTF8String)),
.value = (void *)iconPath.UTF8String,
},
};
AuthorizationEnvironment environment = {
.count = sizeof(environmentItems) / sizeof(*environmentItems),
.items = environmentItems,
};
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return nil;
}]
setNameWithFormat:@"+shipItAuthorization"];
}
- (instancetype)initWithRootPrivileges:(BOOL)rootPrivileges {
self = [self init];
if (self == nil) return nil;
_privileged = rootPrivileges;
return self;
}
- (RACSignal *)sendRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
return [[[self
submitInstallerJobForRequestIfNeeded:request]
concat:[RACSignal defer:^{
}]]
setNameWithFormat:@"%@ -sendRequest: %@", self, request];
}
- (RACSignal *)submitInstallerJobForRequestIfNeeded:(SQRLShipItRequest *)request {
// TODO implement lazy submission when the job is already loaded in launchd
CFStringRef domain = NULL; RACSignal *authorization;
if (self.privileged) {
domain = kSMDomainSystemLaunchd;
authorization = self.class.shipItAuthorization;
} else {
domain = kSMDomainUserLaunchd;
authorization = [RACSignal return:nil];
}
return [[[RACSignal
zip:@[
self.class.shipItInstallerJobDictionary,
authorization,
] reduce:^(NSDictionary *job, SQRLAuthorization *authorization) {
return [self submitJob:job domain:(__bridge id)domain authorization:authorization];
}]
flatten]
setNameWithFormat:@"%@ -submitInstallerJobForRequestIfNeeded: %@", self, request];
}
- (RACSignal *)submitJob:(NSDictionary *)job domain:(NSString *)domain authorization:(SQRLAuthorization *)authorizationValue {
return [[RACSignal
defer:^{
NSString *jobLabel = job[@(LAUNCH_JOBKEY_LABEL)];
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove((__bridge CFStringRef)domain, (__bridge CFStringRef)jobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job %@: %@", jobLabel, error);
}
}
if (!SMJobSubmit((__bridge CFStringRef)domain, (__bridge CFDictionaryRef)job, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -submitJob: %@ domain: %@ authorization: %@", self, job, domain, authorizationValue];
}
@end
+28
View File
@@ -0,0 +1,28 @@
//
// SQRLShipItLauncher.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
// Responsible for launching the ShipIt service to actually install an update.
@interface SQRLShipItLauncher : NSObject
// Returns the label for the ShipIt launchd job.
+ (NSString *)shipItJobLabel;
// Attempts to launch ShipIt.
//
// privileged - Determines which launchd domain to launch the job in.
// If YES, ShipIt is launched in the root domain, otherwise it is
// launched in the current user’s domain.
//
// Returns a signal which will complete, or error, on a background scheduler.
+ (RACSignal *)launchPrivileged:(BOOL)privileged;
@end
+152
View File
@@ -0,0 +1,152 @@
//
// SQRLShipItLauncher.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLShipItLauncher.h"
#import "EXTScope.h"
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "Squirrel-Constants.h"
@implementation SQRLShipItLauncher
+ (NSString *)shipItJobLabel {
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
}
+ (RACSignal *)shipItJobDictionary {
NSString *jobLabel = self.shipItJobLabel;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
return [directoryManager applicationSupportURL];
}]
map:^(NSURL *appSupportURL) {
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
NSMutableDictionary *jobDict = [NSMutableDictionary dictionary];
jobDict[@(LAUNCH_JOBKEY_LABEL)] = jobLabel;
jobDict[@(LAUNCH_JOBKEY_NICE)] = @(-1);
jobDict[@(LAUNCH_JOBKEY_ENABLETRANSACTIONS)] = @NO;
jobDict[@(LAUNCH_JOBKEY_THROTTLEINTERVAL)] = @2;
jobDict[@(LAUNCH_JOBKEY_KEEPALIVE)] = @{
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
};
jobDict[@(LAUNCH_JOBKEY_MACHSERVICES)] = @{
jobLabel: @YES
};
NSMutableArray *arguments = [[NSMutableArray alloc] init];
[arguments addObject:[squirrelBundle URLForResource:@"ShipIt" withExtension:nil].path];
// Pass in the service name so ShipIt knows how to broadcast itself.
[arguments addObject:jobLabel];
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = arguments;
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
#if DEBUG
jobDict[@(LAUNCH_JOBKEY_DEBUG)] = @YES;
#endif
return jobDict;
}]
setNameWithFormat:@"+shipItJobDictionary"];
}
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
},
};
AuthorizationRights rights = {
.count = sizeof(rightItems) / sizeof(*rightItems),
.items = rightItems,
};
NSString *prompt = NSLocalizedString(@"An update is ready to install.", @"SQRLShipItLauncher, launch shipit, authorization prompt");
NSString *iconName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
NSString *iconPath = (iconName == nil ? nil : [NSBundle.mainBundle.resourceURL URLByAppendingPathComponent:iconName].path);
AuthorizationItem environmentItems[] = {
{
.name = kAuthorizationEnvironmentPrompt,
.valueLength = strlen(prompt.UTF8String),
.value = (void *)prompt.UTF8String,
},
{
.name = kAuthorizationEnvironmentIcon,
.valueLength = iconPath == nil ? 0 : strlen(iconPath.UTF8String),
.value = (void *)iconPath.UTF8String,
},
};
AuthorizationEnvironment environment = {
.count = sizeof(environmentItems) / sizeof(*environmentItems),
.items = environmentItems,
};
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return [RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
+ (RACSignal *)launchPrivileged:(BOOL)privileged {
return [[[RACSignal
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, id authorization) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
#if DEBUG
NSLog(@"Could not remove previous ShipIt job: %@", cfError);
#endif
if (cfError != NULL) {
CFRelease(cfError);
cfError = NULL;
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
return [RACSignal empty];
}]
flatten]
setNameWithFormat:@"+launchPrivileged: %i", (int)privileged];
}
@end
-106
View File
@@ -1,106 +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 URL.
//
// URL - The file location to read from. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
// complete, or error.
+ (RACSignal *)readFromURL:(NSURL *)URL;
// Reads a `SQRLShipItState` from encoded data.
//
// data - Serialised request from `serialization`.
//
// Returns a signal which decodes the serialisation and sends a
// `SQRLShipItRequest` then completes, or errors.
+ (RACSignal *)readFromData:(NSData *)data;
// 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;
// Writes the receiver's serialization to disk, at the location specified by the
// URL.
//
// URL - The file location to write to. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeToURL:(NSURL *)URL;
// Encode the receiver for saving to disk or sending over IPC.
//
// Returns a signal which sends a `NSData` then completes, or errors.
- (RACSignal *)serialization;
@end
-189
View File
@@ -1,189 +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 *)readFromURL:(NSURL *)URL {
NSParameterAssert(URL != nil);
return [[[[RACSignal
defer:^{
NSError *error;
NSData *data = [self readFromURL:URL 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 *)writeToURL:(NSURL *)URL {
NSParameterAssert(URL != nil);
return [[[[self
serialization]
flattenMap:^(NSData *data) {
NSError *error;
BOOL write = [self writeData:data toURL:URL error:&error];
if (!write) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
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
+105
View File
@@ -0,0 +1,105 @@
//
// SQRLShipItState.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-08.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
// 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.
// SQRLInstallerStateRelaunching - Relaunching the updated application.
// This state will be entered even if
// there's no relaunching to do.
//
// 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,
SQRLInstallerStateRelaunching
} 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
+118
View File
@@ -0,0 +1,118 @@
//
// 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>
#import "Squirrel-Constants.h"
NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErrorKey";
@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:SQRLErrorDomain 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:SQRLErrorDomain 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:SQRLErrorDomain 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
+32
View File
@@ -0,0 +1,32 @@
//
// SQRLTerminationListener.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-04.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
// Waits for the termination of a GUI application.
@interface SQRLTerminationListener : NSObject
// Initializes the receiver to wait for termination of the app at the given
// location.
//
// bundleURL - The URL to the application bundle to watch. This must not be nil.
// bundleID - The identifier of the application bundle to watch. This must not
// be nil.
- (id)initWithURL:(NSURL *)bundleURL bundleIdentifier:(NSString *)bundleID;
// Lazily waits for termination of all instances of the application identified
// at initialization.
//
// Returns a signal which send an `NSRunningApplication` for each instance of the
// application that is being watched (before the instance terminates), then
// completes on a background scheduler.
- (RACSignal *)waitForTermination;
@end
+89
View File
@@ -0,0 +1,89 @@
//
// SQRLTerminationListener.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-04.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTerminationListener.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLTerminationListener ()
@property (nonatomic, copy, readonly) NSURL *bundleURL;
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
// Waits for the process identified by the given PID to terminate.
//
// Returns a signal which sends `processIdentifier` as soon as the process is
// being monitored, then completes once it exits, all on a background thread.
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier;
@end
@implementation SQRLTerminationListener
#pragma mark Lifecycle
- (id)initWithURL:(NSURL *)bundleURL bundleIdentifier:(NSString *)bundleID {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(bundleID != nil);
self = [super init];
if (self == nil) return nil;
_bundleURL = bundleURL.URLByStandardizingPath;
_bundleIdentifier = [bundleID copy];
return self;
}
#pragma mark Termination Listening
- (RACSignal *)waitForTermination {
return [[[[RACSignal
defer:^{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
return [apps.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
}]
filter:^(NSRunningApplication *application) {
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
}]
flattenMap:^(NSRunningApplication *application) {
return [[self
waitForTerminationOfProcessIdentifier:application.processIdentifier]
mapReplace:application];
}]
setNameWithFormat:@"%@ -waitForTermination", self];
}
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, processIdentifier, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_registration_handler(source, ^{
[subscriber sendNext:@(processIdentifier)];
});
dispatch_source_set_event_handler(source, ^{
[subscriber sendCompleted];
});
dispatch_resume(source);
return [RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
dispatch_release(source);
}];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
}
#pragma mark NSObject
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>{ bundleURL: %@, bundleIdentifier: %@ }", self.class, self, self.bundleURL, self.bundleIdentifier];
}
@end
+2 -16
View File
@@ -134,33 +134,19 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
- (BOOL)validateUpdateURL:(NSURL **)updateURLPtr error:(NSError **)error {
NSURL *updateURL = *updateURLPtr;
if (![updateURL isKindOfClass:NSURL.class]) {
if (![updateURL isKindOfClass:NSURL.class] || updateURL.scheme == nil || updateURL.host == nil || updateURL.path == nil) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An invalid updateURL was given to SQRLUpdate: %@", nil), updateURL]
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
BOOL valid = (updateURL.scheme != nil);
valid &= ([updateURL.scheme isEqualToString:@"file"] || updateURL.host != nil);
valid &= (updateURL.path != nil);
if (!valid) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Update URLs must have a scheme, a host and a path: %@", nil), updateURL]
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
return YES;
}
+5 -63
View File
@@ -6,49 +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;
// The downloaded update does not contain an app bundle, or it was deleted on
// disk before we could get to it.
extern const NSInteger SQRLUpdaterErrorMissingUpdateBundle;
// An error occurred in the out-of-process updater while it was setting up.
extern const NSInteger SQRLUpdaterErrorPreparingUpdateJob;
// The code signing requirement for the running application could not be
// retrieved.
extern const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement;
// The server sent a response that we didn't understand.
//
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidServerResponse;
// The server sent a response body that we didn't understand.
//
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidServerBody;
// The server sent update JSON that we didn't understand.
//
// Includes `SQRLUpdaterJSONObjectErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidJSON;
// Associated with the `NSData` received from the server when an error with code
// `SQRLUpdaterErrorInvalidServerResponse` is generated.
extern NSString * const SQRLUpdaterServerDataErrorKey;
@@ -69,11 +26,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.
//
@@ -81,6 +33,11 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// flattened for convenience.
@property (nonatomic, strong, readonly) RACSignal *updates;
// Whether or not to relaunch after installing an update.
//
// This will be reset to NO whenever update installation fails.
@property (atomic) BOOL shouldRelaunch;
// The request that will be sent to check for updates.
//
// The default value is the argument that was originally passed to
@@ -118,21 +75,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// checking.
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval;
// Terminates the running application to install any available update, then
// automatically relaunches the app after updating.
//
// This method is only useful if you want the application to automatically
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
// other exit mechanism.
//
// After invoking this method, the receiver is responsible for terminating the
// application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
// wrong before termination. The signal will never complete.
- (RACSignal *)relaunchToInstallUpdate;
@end
@interface SQRLUpdater (Unavailable)
+60 -186
View File
@@ -14,49 +14,27 @@
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLShipItConnection.h"
#import "SQRLShipItRequest.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLUpdate.h"
#import "SQRLZipArchiver.h"
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "Squirrel-Constants.h"
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey";
const NSInteger SQRLUpdaterErrorMissingUpdateBundle = 2;
const NSInteger SQRLUpdaterErrorPreparingUpdateJob = 3;
const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement = 4;
const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
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;
// When executed with an `SQRLShipItState`, launches ShipIt.
@property (nonatomic, strong, readonly) RACCommand *shipItLauncher;
// Lazily removes outdated temporary directories (used for previous updates)
// upon first subscription.
// Lazily launches ShipIt upon first subscription.
//
// Pruning directories while an update is pending or in progress will result in
// undefined behavior.
//
// Sends each removed directory then completes, or errors, on an unspecified
// thread.
@property (nonatomic, strong, readonly) RACSignal *prunedUpdateDirectories;
// Sends completed or error.
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
// Parses an update model from downloaded data.
//
@@ -155,46 +133,11 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
if (_signature == nil) {
#if DEBUG
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
return nil;
#else
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
#endif
}
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_prunedUpdateDirectories = [[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSFileManager *manager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:appSupportURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, appSupportURL, error);
return YES;
}];
return [[enumerator.rac_sequence.signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
}]
doNext:^(NSURL *directoryURL) {
NSError *error = nil;
if (![manager removeItemAtURL:directoryURL error:&error]) {
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
}
}];
}]
replayLazily]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
@@ -203,72 +146,37 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
// Prune old updates before the first update check.
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:request]
reduceEach:^(id _, NSData *data) {
return data;
}]
then:^{
self.state = SQRLUpdaterStateCheckingForUpdate;
return [NSURLConnection rac_sendAsynchronousRequest:request];
}]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [[RACSignal
defer:^{
self.state = SQRLUpdaterStateDownloadingUpdate;
return [self downloadAndPrepareUpdate:update];
}]
doCompleted:^{
self.state = SQRLUpdaterStateAwaitingRelaunch;
}];
return [self downloadAndPrepareUpdate:update];
}]
finally:^{
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return;
self.state = SQRLUpdaterStateIdle;
doError:^(id _) {
self.shouldRelaunch = NO;
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
_shipItLauncher = [[RACCommand alloc] initWithSignalBlock:^(SQRLShipItRequest *request) {
NSURL *targetURL = request.targetBundleURL;
_shipItLauncher = [[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
NSNumber *targetWritable = nil;
NSError *targetWritableError = nil;
BOOL gotWritable = [targetURL getResourceValue:&targetWritable forKey:NSURLIsWritableKey error:&targetWritableError];
NSNumber *targetWritable = nil;
NSError *targetWritableError = nil;
BOOL gotWritable = [targetURL getResourceValue:&targetWritable forKey:NSURLIsWritableKey error:&targetWritableError];
// If we can't determine whether it can be written, assume
// nonprivileged and wait for another, more canonical error.
SQRLShipItConnection *connection = [[SQRLShipItConnection alloc] initWithRootPrivileges:(gotWritable && !targetWritable.boolValue)];
return [connection sendRequest:request];
}];
// If we can't determine whether it can be written, assume nonprivileged and
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
replayLazily]
setNameWithFormat:@"shipItLauncher"];
return self;
}
@@ -308,7 +216,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
userInfo[SQRLUpdaterServerDataErrorKey] = data;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:userInfo]];
}
Class updateClass = self.updateClass;
@@ -325,7 +233,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
userInfo[SQRLUpdaterJSONObjectErrorKey] = JSON;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
}
return [RACSignal return:update];
@@ -364,25 +272,11 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
return [[[[NSURLConnection
return [[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [RACSignal return:bodyData];
reduceEach:^(id _, NSData *data) {
return data;
}]
flatten]
flattenMap:^(NSData *data) {
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
@@ -398,14 +292,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [[SQRLZipArchiver
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
@@ -418,11 +305,11 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -482,7 +369,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not locate update bundle for %@ within %@", nil), NSRunningApplication.currentApplication.bundleIdentifier, directory],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
}
}]
map:^(NSURL *URL) {
@@ -491,11 +378,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
setNameWithFormat:@"%@ -applicationBundleMatchingCurrentApplicationInDirectory: %@", self, directory];
}
- (RACSignal *)shipItStateURL {
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return directoryManager.shipItStateURL;
}
#pragma mark Installing Updates
- (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle *)updateBundle {
@@ -521,44 +403,36 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [[[[RACSignal
defer:^{
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
RACSignal *stateLocation = directoryManager.shipItStateURL;
return [[[[SQRLShipItState
readUsingURL:stateLocation]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
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 [[self.shipItStateURL
flattenMap:^(NSURL *location) {
return [request writeToURL:location];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal empty];
}]
concat:[RACSignal return:request]];
then:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
state.relaunchAfterInstallation = self.shouldRelaunch;
return [state writeUsingURL:stateLocation];
}];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [self.shipItLauncher execute:request];
then:^{
return self.shipItLauncher;
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing update", nil) description:NSLocalizedString(@"An update for %@ is being prepared. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier]
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[self.shipItStateURL
flattenMap:^(NSURL *location) {
return [[[[SQRLShipItRequest
readFromURL:location]
map:^(SQRLShipItRequest *request) {
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [request writeToURL:location];
}]
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];
}]
deliverOn:RACScheduler.mainThreadScheduler]
doCompleted:^{
[NSApp terminate:self];
}]
// Never allow `completed` to escape this signal chain (in case
// -terminate: is asynchronous or something crazy).
concat:[RACSignal never]]
replay]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
@end
-7
View File
@@ -10,17 +10,10 @@
@class RACSignal;
extern NSString * const SQRLZipArchiverErrorDomain;
// Associated with an NSNumber containing the code that a shell task exited
// with.
extern NSString * const SQRLZipArchiverExitCodeErrorKey;
// `SQRLZipArchiver` tried to invoke the shell and failed.
//
// Contains `SQRLZipArchiverExitStatusErrorKey` in the `userInfo` dictionary.
extern const NSInteger SQRLZipArchiverShellTaskFailed;
// Uses `ditto` on the command line to zip and unzip archives.
@interface SQRLZipArchiver : NSObject
+2 -3
View File
@@ -9,10 +9,9 @@
#import "SQRLZipArchiver.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "Squirrel-Constants.h"
NSString * const SQRLZipArchiverErrorDomain = @"SQRLZipArchiverErrorDomain";
NSString * const SQRLZipArchiverExitCodeErrorKey = @"SQRLZipArchiverExitCodeErrorKey";
const NSInteger SQRLZipArchiverShellTaskFailed = 1;
@interface SQRLZipArchiver () {
RACSubject *_taskTerminated;
@@ -143,7 +142,7 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}];
}]
take:1]
+122
View File
@@ -0,0 +1,122 @@
//
// main.m
// shipit
//
// Created by Alan Rogers on 29/07/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
// The maximum number of times ShipIt should run the same installation state, in
// an attempt to update.
//
// If ShipIt is launched in the same state more than this number of times,
// updating will abort.
static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
// Waits for all instances of the target application (as described in the
// `state`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
return [[RACSignal
defer:^{
if (state.bundleIdentifier == nil) return [RACSignal empty];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
return [listener waitForTermination];
}]
setNameWithFormat:@"waitForTerminationIfNecessary"];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
NSLog(@"ShipIt quitting");
});
if (argc < 2) {
NSLog(@"Missing launchd job label for ShipIt");
return EXIT_FAILURE;
}
const char *jobLabel = argv[1];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
RACSignal *stateLocation = directoryManager.shipItStateURL;
[[[[[[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);
if (attempt > SQRLShipItMaximumInstallationAttempts) {
return [[[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 {
return [[[[[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];
}
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
dispatch_main();
}
return EXIT_SUCCESS;
}
-170
View File
@@ -1,170 +0,0 @@
//
// main.m
// shipit
//
// Created by Alan Rogers on 29/07/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLInstaller+Private.h"
#import "SQRLShipItRequest.h"
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
typedef NS_ENUM(NSInteger, SQRLShipItError) {
SQRLShipItErrorUnknownAction,
SQRLShipItErrorMissingRequestData,
};
static void reply(xpc_object_t response) {
xpc_connection_t connection = xpc_dictionary_get_remote_connection(response);
xpc_connection_send_message(connection, response);
}
static void replyWithError(xpc_object_t response, NSError *error) {
xpc_dictionary_set_bool(response, "success", false);
if (error != nil) {
xpc_object_t errorDictionary = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(errorDictionary, "domain", error.domain.UTF8String);
xpc_dictionary_set_int64(errorDictionary, "code", error.code);
xpc_dictionary_set_string(errorDictionary, "description", error.localizedDescription.UTF8String);
xpc_dictionary_set_string(errorDictionary, "failureReason", error.localizedFailureReason.UTF8String);
xpc_dictionary_set_string(errorDictionary, "recoverySuggestion", error.localizedRecoverySuggestion.UTF8String);
xpc_dictionary_set_value(response, "error", errorDictionary);
xpc_release(errorDictionary);
}
reply(response);
}
static void replyWithSuccess(xpc_object_t response) {
xpc_dictionary_set_bool(response, "success", true);
reply(response);
}
// Client requests from peer connections.
//
// applicationIdentifier - Current process reverse DNS identifier.
// request - XPC dictionary request object.
//
// Returns nothing.
static void handleRequest(NSString *applicationIdentifier, xpc_object_t request) {
xpc_object_t response = xpc_dictionary_create_reply(request);
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
char const *action = xpc_dictionary_get_string(request, "action");
if (strcmp(action, "install") != 0) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot process \"%s\" requests", action],
};
replyWithError(response, [NSError errorWithDomain:SQRLShipItErrorDomain code:SQRLShipItErrorUnknownAction userInfo:errorInfo]);
return;
}
size_t length;
void const *requestBytes = xpc_dictionary_get_data(request, "request", &length);
if (requestBytes == NULL) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot process \"%s\" requests without \"request\" data", action],
};
replyWithError(response, [NSError errorWithDomain:SQRLShipItErrorDomain code:SQRLShipItErrorUnknownAction userInfo:errorInfo]);
return;
}
[[[SQRLShipItRequest
readFromData:[NSData dataWithBytes:requestBytes length:length] ]
flattenMap:^(SQRLShipItRequest *request) {
return [[installer.installUpdateCommand
execute:request]
catch:^(NSError *error) {
return [[[installer.abortInstallationCommand
execute:nil]
doCompleted:^{
NSLog(@"Abort completed successfully");
}]
concat:[RACSignal error:error]];
}];
}]
subscribeError:^(NSError *error){
NSLog(@"Installation failed with error: %@ %@", error, error.userInfo);
replyWithError(response, error);
} completed:^{
NSLog(@"Installation completed successfully");
replyWithSuccess(response);
}];
}
// Peer connections from the listener connection.
//
// applicationIdentifier - Current process reverse DNS identifier.
// newConnection - The newly accepted connection from the listener.
//
// Returns nothing.
static void handleConnection(NSString *applicationIdentifier, xpc_connection_t newConnection) {
xpc_connection_set_event_handler(newConnection, ^(xpc_object_t object) {
xpc_type_t type = xpc_get_type(object);
if (type == XPC_TYPE_ERROR) {
char *description = xpc_copy_description(object);
NSLog(@"XPC peer error: %s", description);
free(description);
return;
}
if (type != XPC_TYPE_DICTIONARY) {
NSLog(@"XPC peer expected dictionary");
return;
}
handleRequest(applicationIdentifier, object);
});
xpc_connection_resume(newConnection);
}
// Listens for XPC messages from clients.
//
// Arguments are expected in the following order:
//
// jobLabel - The launchd job label for this task.
//
// Returns 0 on successful termination, non 0 otherwise.
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
NSLog(@"ShipIt quitting");
});
if (argc < 2) {
NSLog(@"Missing launchd job label for ShipIt");
return EXIT_FAILURE;
}
char const *jobLabel = argv[1];
NSString *applicationIdentifier = @(jobLabel);
xpc_connection_t server = xpc_connection_create_mach_service(jobLabel, NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
xpc_connection_set_event_handler(server, ^(xpc_object_t object) {
xpc_type_t type = xpc_get_type(object);
if (type == XPC_TYPE_ERROR) {
char *description = xpc_copy_description(object);
NSLog(@"XPC listener error: %s", description);
free(description);
} else if (type == XPC_TYPE_CONNECTION) {
handleConnection(applicationIdentifier, object);
}
});
xpc_connection_resume(server);
dispatch_main();
}
return EXIT_SUCCESS;
}
+71
View File
@@ -0,0 +1,71 @@
//
// Squirrel-Constants.h
// Squirrel
//
// Created by Keith Duncan on 30/10/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
// Error domain for errors originating in Squirrel.
extern NSString * const SQRLErrorDomain;
// Error codes for errors originating in Squirrel.
//
// SQRLCodeSignatureErrorDidNotPass - The bundle did not pass codesign
// verification.
//
// SQRLCodeSignatureErrorCouldNotCreateStaticCode - A static code object could
// not be created for the target bundle or running code.
//
// SQRLShipItStateErrorMissingRequiredProperty - A required property was `nil`
// upon initialization. Includes `SQRLShipItStatePropertyErrorKey` in the
// error's `userInfo` dictionary.
//
// SQRLShipItStateErrorUnarchiving - The saved state on disk could not be
// unarchived, possibly because it's invalid.
//
// SQRLShipItStateErrorArchiving - The state object could not be archived.
//
// SQRLUpdaterErrorMissingUpdateBundle - The downloaded update does not contain
// an app bundle, or it was deleted on disk before we could get to it.
//
// SQRLUpdaterErrorPreparingUpdateJob - An error occurred in the out-of-process
// updater while it was setting up.
//
// SQRLUpdaterErrorRetrievingCodeSigningRequirement - The code signing
// requirement for the running application could not be retrieved.
//
// SQRLUpdaterErrorInvalidServerResponse - The server sent a response that we
// didn't understand. Includes `SQRLUpdaterServerDataErrorKey` in the error's
// `userInfo` dictionary.
//
// SQRLUpdaterErrorInvalidJSON - The server sent update JSON that we didn't
// understand. Includes `SQRLUpdaterJSONObjectErrorKey` in the error's
// `userInfo` dictionary.
//
// SQRLZipArchiverShellTaskFailed - `SQRLZipArchiver` tried to invoke the shell
// and failed. Includes `SQRLZipArchiverExitStatusErrorKey` in the error's
// `userInfo` dictionary.
//
// SQRLShipItLauncherErrorCouldNotStartService - The ShipIt service could not be
// started.
typedef enum : NSInteger {
SQRLCodeSignatureErrorDidNotPass = -1,
SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2,
SQRLShipItStateErrorMissingRequiredProperty = -100,
SQRLShipItStateErrorUnarchiving = -101,
SQRLShipItStateErrorArchiving = -102,
SQRLUpdaterErrorMissingUpdateBundle = -200,
SQRLUpdaterErrorPreparingUpdateJob = -201,
SQRLUpdaterErrorRetrievingCodeSigningRequirement = -202,
SQRLUpdaterErrorInvalidServerResponse = -203,
SQRLUpdaterErrorInvalidJSON = -204,
SQRLZipArchiverShellTaskFailed = -300,
SQRLShipItLauncherErrorCouldNotStartService = -400,
} SQRLErrorCode;
+11
View File
@@ -0,0 +1,11 @@
//
// Squirrel-Constants.m
// Squirrel
//
// Created by Keith Duncan on 30/10/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "Squirrel-Constants.h"
NSString * const SQRLErrorDomain = @"SQRLErrorDomain";
@@ -7,6 +7,7 @@
//
#import "SQRLCodeSignature.h"
#import "Squirrel-Constants.h"
SpecBegin(SQRLCodeSignature)
@@ -30,11 +31,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();
});
@@ -54,7 +55,7 @@ describe(@"code signature changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -66,7 +67,7 @@ describe(@"code signature changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -80,7 +81,7 @@ describe(@"main executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
});
@@ -92,7 +93,7 @@ describe(@"main executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -113,7 +114,7 @@ describe(@"helper executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -125,7 +126,7 @@ describe(@"helper executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -146,7 +147,7 @@ describe(@"resource changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -158,7 +159,7 @@ describe(@"resource changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -178,7 +179,7 @@ describe(@"framework changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -190,7 +191,7 @@ describe(@"framework changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
+1 -5
View File
@@ -60,16 +60,12 @@ void (^deepCodesignTestApplication)(void) = ^{
NSTask *deepCodesignTask = [[NSTask alloc] init];
deepCodesignTask.launchPath = deepCodesignLocation.path;
deepCodesignTask.standardError = [NSPipe pipe];
deepCodesignTask.standardOutput = [NSPipe pipe];
NSMutableDictionary *environment = environmentSuitableForChildProcess();
[environment addEntriesFromDictionary:@{
@"CODE_SIGN_IDENTITY": @"-",
@"CONFIGURATION_BUILD_DIR": testApplicationLocation.URLByDeletingLastPathComponent.path,
@"FULL_PRODUCT_NAME": testApplicationLocation.lastPathComponent,
}];
deepCodesignTask.environment = environment;
[deepCodesignTask launch];
@@ -110,7 +106,7 @@ it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
xit(@"should deep verify after signing", ^{
it(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
+51 -103
View File
@@ -9,43 +9,22 @@
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLInstaller+Private.h"
#import "SQRLShipItConnection.h"
#import "SQRLShipItRequest.h"
#import "SQRLInstallerOwnedBundle.h"
#import "SQRLShipItLauncher.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,48 @@ 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(modeOfURL(self.testApplicationURL)).to.equal(0755);
expect(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0755);
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
describe(@"signal handling", ^{
@@ -162,9 +112,10 @@ 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];
// Apply a random delay before sending the termination signal, to
// fuzz out race conditions.
@@ -178,36 +129,33 @@ 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();
});
it(@"should handle SIGHUP", ^{
system("killall -HUP shipit-installer");
system("killall -v -HUP ShipIt");
});
it(@"should handle SIGTERM", ^{
system("killall -TERM shipit-installer");
system("killall -v -TERM ShipIt");
});
it(@"should handle SIGINT", ^{
system("killall -INT shipit-installer");
system("killall -v -INT ShipIt");
});
it(@"should handle SIGQUIT", ^{
system("killall -QUIT shipit-installer");
system("killall -v -QUIT ShipIt");
});
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).
// a few times to really test resumption.
for (int i = 0; i < 3; i++) {
system("killall -KILL shipit-installer");
system("killall -v -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
-79
View File
@@ -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 when no file exists yet", ^{
NSError *error;
BOOL success = [[SQRLShipItRequest readFromURL:[directoryManager.shipItStateURL first]] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
});
it(@"should write and read to disk", ^{
NSError *error;
BOOL success = [[request writeToURL:[directoryManager.shipItStateURL first]] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readFromURL:[directoryManager.shipItStateURL first]] 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 readFromURL: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
+63
View File
@@ -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
@@ -0,0 +1,65 @@
//
// SQRLTerminationListenerSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-07.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTerminationListener.h"
SpecBegin(SQRLTerminationListener)
__block SQRLTerminationListener *listener;
beforeEach(^{
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
expect(listener).notTo.beNil();
});
it(@"should complete immediately when the app is not running", ^{
__block BOOL completed = NO;
[[listener waitForTermination] subscribeCompleted:^{
completed = YES;
}];
expect(completed).to.beTruthy();
});
it(@"should wait until one instance terminates", ^{
NSRunningApplication *app = [self launchTestApplicationWithEnvironment:nil];
__block NSRunningApplication *observedApp = nil;
__block BOOL completed = NO;
[[listener waitForTermination] subscribeNext:^(id x) {
observedApp = x;
} completed:^{
completed = YES;
}];
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
expect([app terminate]).to.beTruthy();
expect(completed).will.beTruthy();
});
it(@"should wait until multiple instances terminate", ^{
NSRunningApplication *app1 = [self launchTestApplicationWithEnvironment:nil];
NSRunningApplication *app2 = [self launchTestApplicationWithEnvironment:nil];
__block BOOL completed = NO;
[[listener waitForTermination] subscribeCompleted:^{
completed = YES;
}];
expect(completed).to.beFalsy();
expect([app1 terminate]).to.beTruthy();
expect(completed).to.beFalsy();
expect([app2 terminate]).to.beTruthy();
expect(completed).will.beTruthy();
});
SpecEnd
+7 -13
View File
@@ -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,10 +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.
//
// request - The install to send to ShipIt.
- (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.
//
@@ -87,11 +88,4 @@ extern NSString * const SQRLBundleShortVersionStringKey;
// automatically be unmounted and deleted at the end of the example.
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL;
// Add a block to cleanup after each example has run
//
// Blocks are run in reverse order, i.e. LIFO
//
// block - The block to invoke after the current example has finished
- (void)addCleanupBlock:(dispatch_block_t)block;
@end
+70 -88
View File
@@ -7,13 +7,9 @@
//
#import "SQRLTestCase.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItConnection.h"
#import "SQRLInstaller.h"
#import "SQRLShipItRequest.h"
#import "SQRLTestHelper.h"
#import "SQRLShipItLauncher.h"
#import <ServiceManagement/ServiceManagement.h>
#pragma clang diagnostic push
@@ -55,10 +51,6 @@ static void SQRLSignalHandler(int sig) {
// all copied test data.
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
// Returns an _unlaunched_ task that will follow log files at the given paths,
// then pipe that output through this process.
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths;
@end
@implementation SQRLTestCase
@@ -91,14 +83,15 @@ static void SQRLSignalHandler(int sig) {
[[NSData data] writeToURL:URL atomically:YES];
}
RACSequence *paths = [URLs map:^(NSURL *URL) {
return URL.path;
}];
NSArray *args = [[[URLs
map:^(NSURL *URL) {
return URL.path;
}]
startWith:@"-f"]
array];
NSTask *readShipIt = [self tailTaskWithPaths:paths];
[readShipIt launch];
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
NSTask *readShipIt = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:args];
NSAssert([readShipIt isRunning], @"Could not start task %@ with arguments: %@", readShipIt, args);
atexit_b(^{
[readShipIt terminate];
@@ -114,6 +107,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,25 +128,7 @@ static void SQRLSignalHandler(int sig) {
}
- (void)addCleanupBlock:(dispatch_block_t)block {
[SQRLTestHelper addCleanupBlock:block];
}
#pragma mark Logging
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths {
NSPipe *outputPipe = [NSPipe pipe];
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
outputHandle.readabilityHandler = ^(NSFileHandle *handle) {
NSString *output = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding];
NSLog(@"\n%@", output);
};
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/tail";
task.standardOutput = outputPipe;
task.arguments = [[paths startWith:@"-f"] array];
return task;
[self.exampleCleanupBlocks addObject:[block copy]];
}
#pragma mark Temporary Directory
@@ -148,10 +137,10 @@ static void SQRLSignalHandler(int sig) {
if (_baseTemporaryDirectoryURL == nil) {
NSURL *globalTemporaryDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
_baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
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 +156,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,19 +167,17 @@ 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];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSequence return:testAppLog.path]];
[readTestApp launch];
XCTAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
NSTask *readTestApp = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:@[ @"-f", testAppLog.path ]];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
[readTestApp terminate];
@@ -203,8 +190,8 @@ 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 +210,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 +231,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 +243,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 +262,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 +272,52 @@ 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 = SQRLShipItConnection.shipItJobLabel;
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) {
SQRLShipItConnection *connection = [[SQRLShipItConnection alloc] initWithRootPrivileges:NO];
- (void)launchShipIt {
NSError *error = nil;
STAssertTrue([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error], @"Could not launch ShipIt: %@", error);
__block NSError *error = nil;
expect([[connection sendRequest:request] waitUntilCompleted:&error]).to.beTruthy();
expect(error).to.beNil();
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
CFErrorRef removeError = NULL;
if (!SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, &removeError)) {
NSLog(@"Could not remove ShipIt job after tests: %@", removeError);
if (removeError != NULL) CFRelease(removeError);
}
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItConnection.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) {
-17
View File
@@ -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
-42
View File
@@ -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
+60 -215
View File
@@ -6,12 +6,8 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
#import "SQRLTestUpdate.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "TestAppConstants.h"
#import "SQRLZipArchiver.h"
SpecBegin(SQRLUpdater)
@@ -53,236 +49,85 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should not install a corrupt update", ^{
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager applicationSupportURL] first];
expect(appSupportURL).notTo.beNil();
updateDirectoryURLs = [[RACSignal
defer:^{
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
if (contents == nil) return [RACSignal empty];
return contents.rac_sequence.signal;
}]
filter:^(NSURL *directoryURL) {
return [directoryURL.lastPathComponent hasPrefix:@"update."];
}];
});
it(@"should remove downloaded archives after updating", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([updateDirectoryURLs toArray]).will.equal(@[]);
});
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
it(@"should not install a corrupt update", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
before(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
it(@"should return an error for non 2xx code HTTP responses", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:nil statusCode:/* Server Error */ 500 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
writeUpdate(update);
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
it(@"should return an error for non JSON data", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:NSData.data statusCode:/* OK */ 200 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
});
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
static RACSignal * (^stateNotificationListener)(void) = ^ {
return [[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil]
map:^(NSNotification *notification) {
return notification.userInfo[SQRLTestAppUpdaterStateKey];
}]
setNameWithFormat:@"stateNotificationListener"];
};
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
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];
}];
writeUpdate(update);
NSRunningApplication *testApplication = launchWithEnvironment(nil);
NSRunningApplication *app = launchWithEnvironment(nil);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateIdle),
];
expect(states).will.equal(expectedStates);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
expect(testApplication.terminated).will.beTruthy();
});
NSURL *updateURL = [self createTestApplicationUpdate];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
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];
}];
writeUpdate(update);
NSError *error;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate([self createTestApplicationUpdate]),
@"final": @YES,
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
writeUpdate(update);
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
NSRunningApplication *testApplication = launchWithEnvironment(nil);
writeUpdate(update);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateDownloadingUpdate),
@(SQRLUpdaterStateAwaitingRelaunch),
];
expect(states).will.equal(expectedStates);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
expect(testApplication.terminated).will.beTruthy();
});
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
SpecEnd
+2 -1
View File
@@ -8,6 +8,7 @@
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
#import "Squirrel-Constants.h"
SpecBegin(SQRLZipArchiver)
@@ -35,7 +36,7 @@ it(@"should fail to extract a nonexistent zip archive", ^{
NSLog(@"%@", error);
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
});
+1 -4
View File
@@ -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
-22
View File
@@ -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;
-13
View File
@@ -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";
+2 -9
View File
@@ -8,11 +8,10 @@
#import "TestAppDelegate.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItConnection.h"
#import "SQRLShipItLauncher.h"
#import "SQRLTestUpdate.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "TestAppConstants.h"
@interface TestAppDelegate ()
@@ -35,7 +34,7 @@
NSLog(@"TestApplication quitting");
});
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
NSError *error = nil;
BOOL removed = [[[directoryManager
@@ -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
+6 -68
View File
@@ -1,66 +1,6 @@
# objc-build-scripts
This project is a collection of scripts created with two goals:
1. To standardize how Objective-C projects are bootstrapped after cloning
1. To easily build Objective-C projects on continuous integration servers
## Scripts
Right now, there are two important scripts: [`bootstrap`](#bootstrap) and
[`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and
eliminate pesky system configuration issues (like setting up a working Ruby
environment).
The structure of the scripts on disk is meant to follow that of a typical Ruby
project:
```
script/
bootstrap
cibuild
```
### bootstrap
This script is responsible for bootstrapping (initializing) your project after
it's been checked out. Here, you should install or clone any dependencies that
are required for a working build and development environment.
By default, the script will verify that [xctool][] is installed, then initialize
and update submodules recursively. If any submodules contain `script/bootstrap`,
that will be run as well.
To check that other tools are installed, you can set the `REQUIRED_TOOLS`
environment variable before running `script/bootstrap`, or edit it within the
script directly. Note that no installation is performed automatically, though
this can always be added within your specific project.
### cibuild
This script is responsible for building the project, as you would want it built
for continuous integration. This is preferable to putting the logic on the CI
server itself, since it ensures that any changes are versioned along with the
source.
By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode
workspace or project in the working directory, then build all targets/schemes
(as found by `xcodebuild -list`) using [xctool][].
You can also specify the schemes to build by passing them into the script:
```sh
script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS
```
As with the `bootstrap` script, there are several environment variables that can
be used to customize behavior. They can be set on the command line before
invoking the script, or the defaults changed within the script directly.
## Getting Started
To add the scripts to your project, read the contents of this repository into
a `script` folder:
These scripts are primarily meant to support the use of
[Janky](https://github.com/github/janky). To use them, read the contents of this
repository into a `script` folder:
```
$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
@@ -68,15 +8,13 @@ $ git fetch objc-build-scripts
$ git read-tree --prefix=script/ -u objc-build-scripts/master
```
Then commit the changes, to incorporate the scripts into your own repository's
Then commit the changes to incorporate the scripts into your own repository's
history. You can also freely tweak the scripts for your specific project's
needs.
To merge in upstream changes later:
To bring in upstream changes later:
```
$ git fetch -p objc-build-scripts
$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
$ git merge -Xsubtree=script objc-build-scripts/master
```
[xctool]: https://github.com/facebook/xctool
+7 -69
View File
@@ -1,73 +1,11 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
##
## Configuration Variables
##
set -o errexit
config ()
{
# A whitespace-separated list of executables that must be present and locatable.
: ${REQUIRED_TOOLS="xctool"}
export REQUIRED_TOOLS
}
##
## Bootstrap Process
##
main ()
{
config
if [ -n "$REQUIRED_TOOLS" ]
then
echo "*** Checking dependencies..."
check_deps
fi
local submodules=$(git submodule status 2>/dev/null)
if [ -n "$submodules" ]
then
echo "*** Updating submodules..."
update_submodules
fi
}
check_deps ()
{
for tool in $REQUIRED_TOOLS
do
which -s "$tool"
if [ "$?" -ne "0" ]
then
echo "*** Error: $tool not found. Please install it and bootstrap again."
exit 1
fi
done
}
bootstrap_submodule ()
{
local bootstrap="script/bootstrap"
if [ -e "$bootstrap" ]
then
echo "*** Bootstrapping $name..."
"$bootstrap" >/dev/null
else
update_submodules
fi
}
update_submodules ()
{
git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule
}
export -f bootstrap_submodule
export -f update_submodules
main
echo "*** Updating submodules..."
git submodule sync --quiet
git submodule update --init
git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init"
+80 -113
View File
@@ -1,162 +1,129 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
##
## Configuration Variables
##
SCHEMES="$@"
# The build configuration to use.
if [ -z "$XCCONFIGURATION" ]
then
XCCONFIGURATION="Test"
fi
config ()
{
# The workspace to build.
#
# If not set and no workspace is found, the -workspace flag will not be passed
# to `xctool`.
#
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
# take precedence.
: ${XCWORKSPACE=$(find_pattern "*.xcworkspace")}
# The workspace to build.
#
# If not set and no workspace is found, the -workspace flag will not be passed
# to xcodebuild.
if [ -z "$XCWORKSPACE" ]
then
XCWORKSPACE=$(ls -d *.xcworkspace 2>/dev/null | head -n 1)
fi
# The project to build.
#
# If not set and no project is found, the -project flag will not be passed
# to `xctool`.
#
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
# take precedence.
: ${XCODEPROJ=$(find_pattern "*.xcodeproj")}
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
BOOTSTRAP="$SCRIPT_DIR/bootstrap"
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
: ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"}
# A whitespace-separated list of default targets or schemes to build, if none
# are specified on the command line.
#
# Individual names can be quoted to avoid word splitting.
DEFAULT_TARGETS=Squirrel
# Extra options to pass to xctool.
: ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"}
# A whitespace-separated list of default schemes to build.
#
# Individual names can be quoted to avoid word splitting.
: ${SCHEMES:=Squirrel}
# A source-able file with information about the Keychain, containing our
# code-signing certificate.
: ${KEYCHAIN_INFO:=/var/lib/jenkins/config/xcodekeychain}
export XCWORKSPACE
export XCODEPROJ
export BOOTSTRAP
export XCTOOL_OPTIONS
export SCHEMES
export KEYCHAIN_INFO
}
# Extra build settings to pass to xcodebuild.
XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
##
## Code Signing
## Code-signing Setup
##
unlock_keychain ()
{
if [ -e "$KEYCHAIN_INFO" ]
then
# Unlock the keychain so the certificates it contains can be used for building.
. "$KEYCHAIN_INFO"
security unlock-keychain -p "$XCODE_KEYCHAIN_PASSWORD" "$XCODE_KEYCHAIN"
fi
}
# A source-able file with information about the Keychain containing our
# code-signing certificate.
KEYCHAIN_INFO=/var/lib/jenkins/config/xcodekeychain
if [ -e "$KEYCHAIN_INFO" ]
then
# Unlock the keychain so the certificates it contains can be used for building.
. "${KEYCHAIN_INFO}"
security unlock-keychain -p "${XCODE_KEYCHAIN_PASSWORD}" "${XCODE_KEYCHAIN}"
fi
##
## Build Process
##
main ()
{
config
unlock_keychain
if [ -f "$BOOTSTRAP" ]
if [ -z "$*" ]
then
# lol recursive shell script
if [ -n "$DEFAULT_TARGETS" ]
then
echo "*** Bootstrapping..."
"$BOOTSTRAP" || exit $?
echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
else
xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
fi
echo "*** The following schemes will be built:"
echo "$SCHEMES" | xargs -n 1 echo " "
echo
exit $?
fi
echo "$SCHEMES" | xargs -n 1 | (
local status=0
if [ -f "$BOOTSTRAP" ]
then
echo "*** Bootstrapping..."
bash "$BOOTSTRAP" || exit $?
fi
while read scheme
do
build_scheme "$scheme" || status=1
done
echo "*** The following targets will be built:"
exit $status
)
}
for target in "$@"
do
echo "$target"
done
find_pattern ()
echo "*** Cleaning all targets..."
xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
run_xcodebuild ()
{
ls -d $1 2>/dev/null | head -n 1
}
local scheme=$1
run_xctool ()
{
if [ -n "$XCWORKSPACE" ]
then
xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1
elif [ -n "$XCODEPROJ" ]
then
xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1
xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
else
echo "*** No workspace or project file found."
exit 1
xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
fi
}
parse_build ()
{
awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null
local status=$?
return $status
}
build_scheme ()
{
local scheme=$1
echo "*** Cleaning $scheme..."
run_xctool -scheme "$scheme" clean >/dev/null || exit $?
echo "*** Building and testing $scheme..."
echo
local sdkflag=
local action=test
# Determine whether we can run unit tests for this target.
run_xctool -scheme "$scheme" run-tests | parse_build
run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
local awkstatus=$?
local xcstatus=${PIPESTATUS[0]}
if [ "$awkstatus" -ne "0" ]
if [ "$xcstatus" -eq "65" ]
then
# Unit tests aren't supported.
action=build
# This probably means that there's no scheme by that name. Give up.
echo "*** Error building scheme $scheme -- perhaps it doesn't exist"
elif [ "$awkstatus" -eq "1" ]
then
return $awkstatus
fi
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
return $xcstatus
}
export -f build_scheme
export -f run_xctool
export -f parse_build
echo "*** Building..."
main
for scheme in "$@"
do
build_scheme "$scheme" || exit $?
done
-12
View File
@@ -1,12 +0,0 @@
BEGIN {
FS = "\n";
}
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}
}
-25
View File
@@ -1,25 +0,0 @@
# Exit statuses:
#
# 0 - No errors found.
# 1 - Wrong SDK. Retry with SDK `iphonesimulator`.
# 2 - Missing target.
BEGIN {
status = 0;
}
{
print;
}
/Testing with the '(.+)' SDK is not yet supported/ {
status = 1;
}
/does not contain a target named/ {
status = 2;
}
END {
exit status;
}