2 Commits
Author SHA1 Message Date
Justin Spahr-Summers 7c89431c4a ++RAC for +rac_sendAsynchronousRequest: fix 2014-02-26 09:45:05 -08:00
Justin Spahr-Summers 2e33ff84f9 ++RAC to v2.2.4 2014-02-17 11:28:00 -08:00
76 changed files with 2452 additions and 2572 deletions
+6 -19
View File
@@ -1,22 +1,9 @@
[submodule "External/ReactiveCocoa"]
path = External/ReactiveCocoa
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
[submodule "Carthage.checkout/Nimble"]
path = Carthage/Checkouts/Nimble
url = https://github.com/Quick/Nimble.git
[submodule "Carthage.checkout/ReactiveCocoa"]
path = Carthage/Checkouts/ReactiveCocoa
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "Carthage.checkout/Quick"]
path = Carthage/Checkouts/Quick
url = https://github.com/Quick/Quick.git
[submodule "Carthage.checkout/xcconfigs"]
path = Carthage/Checkouts/xcconfigs
url = https://github.com/jspahrsummers/xcconfigs.git
[submodule "Carthage.checkout/Mantle"]
path = Carthage/Checkouts/Mantle
url = https://github.com/Mantle/Mantle.git
[submodule "Carthage/Checkouts/Nimble"]
url = https://github.com/Quick/Nimble.git
[submodule "Carthage/Checkouts/Quick"]
url = https://github.com/Quick/Quick.git
-12
View File
@@ -1,12 +0,0 @@
osx_image: xcode7.1
language: objective-c
before_install:
- brew update
- brew outdated xctool || brew upgrade xctool
install: script/bootstrap
script: script/cibuild
notifications:
email: false
env:
global:
secure: L4C3hHzXc/sgL35dIz3kjqIzPWnXCHsFOgbOcz0f249zlnegeKHQdChm4HUDtfrbmKLMYQCIvVfcgPeLaHkCSeHqXUihXLT2LglK+0GX3MBbFzde9x1uln2O68hH1tTmBg2O2i4Dtc4D3e+2tVv3XmhQDZt0znIVO6n9YgevDqw=
-2
View File
@@ -1,2 +0,0 @@
github "Mantle/Mantle" ~> 1.5.3
github "ReactiveCocoa/ReactiveCocoa" ~> 2.4.3
-3
View File
@@ -1,3 +0,0 @@
github "jspahrsummers/xcconfigs" >= 0.7.1
github "Quick/Quick" ~> 0.7
github "Quick/Nimble" ~> 2.0
-5
View File
@@ -1,5 +0,0 @@
github "Mantle/Mantle" "1.5.6"
github "Quick/Nimble" "v2.0.0"
github "Quick/Quick" "v0.7.0"
github "ReactiveCocoa/ReactiveCocoa" "v2.5"
github "jspahrsummers/xcconfigs" "0.8.1"
Vendored Submodule
+1
Submodule External/Mantle added at 8aef509d18
Vendored Submodule
+1
Submodule External/ReactiveCocoa added at 09e17b9b9b
+9 -19
View File
@@ -6,7 +6,6 @@ and transparent as updates to a website**.
Instead of publishing a feed of versions from which your app must select,
Squirrel updates to the version your server tells it to. This allows you to
intelligently update your clients based on the request you give to Squirrel.
The server can remotely drive behaviors like rolling back or phased rollouts.
Your request can include authentication details, custom headers or a request
body so that your server has the context it needs in order to supply the most
@@ -27,20 +26,17 @@ updates installed are valid.
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Run `script/bootstrap` from within the submodule
1. Add references to Squirrel.xcodeproj and its [dependencies](#dependencies) to
your project
1. Add a reference to Squirrel.xcodeproj to your project
1. Add Squirrel.framework as a target dependency
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
it into your Frameworks directory
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
does not embed them itself.
If you’re developing Squirrel on its own, then use `Squirrel.xcworkspace`.
# Dependencies
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
and [Mantle](https://github.com/Mantle/Mantle).
and [Mantle](https://github.com/MantleFramework/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
@@ -114,18 +110,12 @@ To know when an update is ready to be installed, you can subscribe to the
}];
```
## Installing Updates
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.
While downloaded updates are automatically installed when your application
terminates, if don't want to wait you can manually terminate the app to begin
the installation process immediately.
Once an [update available notification](#update-available-notifications) has
been received, you may want to present an interface informing the user about
the update and offering the ability to install and relaunch.
To explicitly install a downloaded update and automatically relaunch afterward,
subscribe to the `relaunchToInstallUpdate` signal on `SQRLUpdater`:
If you want to install a downloaded update and automatically relaunch afterward,
`SQRLUpdater` can do that:
```objc
[[self.updater relaunchToInstallUpdate] subscribeError:^(NSError *error) {
@@ -159,7 +149,7 @@ to the update request provided:
"url": "http://mycompany.com/myapp/releases/myrelease",
"name": "My Release Name",
"notes": "Theses are some release notes innit",
"pub_date": "2013-09-18T12:29:53+01:00"
"pub_date": "2013-09-18T12:29:53+01:00",
}
```
@@ -176,4 +166,4 @@ appropriate format.
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-available-notifications).
updates](#update-notifications).
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0710"
version = "2.0">
LastUpgradeVersion = "0500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -22,14 +22,14 @@
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.xctest"
BuildableName = "SquirrelTests.octest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
@@ -37,61 +37,32 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO">
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.xctest"
BuildableName = "SquirrelTests.octest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "FORCE_APP_IDENTIFIER"
value = "com.github.Squirrel.SquirrelTests"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQRLUpdateFromURL"
@@ -103,20 +74,11 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Profile"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0710"
LastUpgradeVersion = "0500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -23,10 +23,10 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
</Testables>
<MacroExpansion>
@@ -38,21 +38,17 @@
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -65,13 +61,12 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0710"
LastUpgradeVersion = "0500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -23,42 +23,30 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
-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:Carthage/Checkouts/Mantle/Mantle.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/ReactiveCocoa/ReactiveCocoa.xcodeproj">
</FileRef>
<FileRef
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Quick/Quick.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
</FileRef>
</Workspace>
@@ -14,7 +14,4 @@
// Info.plist, or nil if the key is not present.
@property (nonatomic, copy, readonly) NSString *sqrl_bundleVersion;
/// The value of the `kCFBundleExecutableKey` key.
@property (nonatomic, copy, readonly) NSString *sqrl_executableName;
@end
@@ -14,8 +14,4 @@
return [self objectForInfoDictionaryKey:(id)kCFBundleVersionKey];
}
- (NSString *)sqrl_executableName {
return [self objectForInfoDictionaryKey:(id)kCFBundleExecutableKey];
}
@end
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <ReactiveCocoa/RACSignal.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface RACSignal (SQRLTransactionExtensions)
@@ -7,8 +7,6 @@
//
#import "RACSignal+SQRLTransactionExtensions.h"
#import <ReactiveCocoa/RACDisposable.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
// How long before power assertions time out.
+8 -20
View File
@@ -21,44 +21,32 @@ extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
@class RACSignal;
// Implements the verification of Apple code signatures and requirements.
// Implements the verification of Apple code signatures.
@interface SQRLCodeSignature : MTLModel
// A serialized version of the `SecRequirementRef` that the receiver was
// initialized with.
@property (nonatomic, copy, readonly) NSData *requirementData;
// Determines the designated requirement of the currently-executing application.
// The returned code signature can be used to verify that a bundle is code sign
// valid and meets the designated requirement of the current application.
// Determines the code signature of the currently-executing application.
//
// error - If not NULL, set to any error that occurs.
//
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
// designated requirement for the running code.
// signature for the running code.
+ (instancetype)currentApplicationSignature:(NSError **)error;
// Determines the designated requirement of the specified bundle. The returned
// code signature can be used to verify that an arbitrary bundle is valid and
// meets the designated requirement of `bundle`.
// Initializes the receiver with the given requirement.
//
// bundleURL - Must not be nil, the location of a bundle directory structure
// which has been code signed and includes a designated requirement.
// This bundle's designated requirement is used when verifying other
// bundles.
// error - If not NULL, set to any error that occurs.
//
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
// designated requirement of the bundle at `bundleURL`.
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)error;
// requirement - The code requirement for tested bundles. This must not be NULL.
- (id)initWithRequirement:(SecRequirementRef)requirement;
// Verifies the code signature of the specified bundle and verifies that the
// bundle meets the receiver's requirement.
// Verifies that the code signature of the specified bundle matches the receiver.
//
// bundleURL - The URL to the bundle to verify on disk. This must not be nil.
//
// Returns a signal which will synchronously send completed on success, or error
// if the requirement was not verified successfully.
// if the code signature was not verified successfully.
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL;
@end
+35 -55
View File
@@ -7,11 +7,8 @@
//
#import "SQRLCodeSignature.h"
#import "EXTKeyPathCoding.h"
#import "EXTScope.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
@@ -22,14 +19,9 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
@interface SQRLCodeSignature ()
// A `SecRequirementRef` that tested bundles must satisfy.
@property (atomic, strong) id requirement;
// Initializes the receiver with the given requirement.
//
// This is the designated initializer for this class.
//
// requirement - The code requirement for tested bundles. This must not be NULL.
- (id)initWithRequirement:(SecRequirementRef)requirement;
// This property is automatically retained.
@property (atomic) id requirement;
@end
@@ -45,49 +37,8 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
#pragma mark Lifecycle
+ (instancetype)currentApplicationSignature:(NSError **)errorRef {
SecCodeRef staticCode = NULL;
OSStatus error = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
if (error != noErr) {
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
return nil;
}
@onExit {
CFRelease(staticCode);
};
return [self signatureWithCode:staticCode error:errorRef];
}
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)errorRef {
SecStaticCodeRef bundleCode = NULL;
OSStatus error = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &bundleCode);
if (error != noErr) {
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
return nil;
}
@onExit {
CFRelease(bundleCode);
};
return [self signatureWithCode:bundleCode error:errorRef];
}
+ (instancetype)signatureWithCode:(SecStaticCodeRef)code error:(NSError **)errorRef {
SecRequirementRef designatedRequirement = NULL;
OSStatus error = SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &designatedRequirement);
if (error != noErr) {
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
return nil;
}
@onExit {
CFRelease(designatedRequirement);
};
return [[SQRLCodeSignature alloc] initWithRequirement:designatedRequirement];
+ (instancetype)currentApplicationSignature:(NSError **)error {
return [self modelWithDictionary:nil error:error];
}
- (id)initWithRequirement:(SecRequirementRef)requirement {
@@ -98,6 +49,35 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
} error:NULL];
}
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [super initWithDictionary:dictionary error:error];
if (self == nil) return nil;
if (self.requirement == nil) {
SecCodeRef staticCode = NULL;
OSStatus result = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
@onExit {
if (staticCode != NULL) CFRelease(staticCode);
};
if (result != noErr) {
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
return nil;
}
SecRequirementRef req = NULL;
result = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &req);
self.requirement = CFBridgingRelease(req);
if (result != noErr) {
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
return nil;
}
}
return self;
}
#pragma mark Verification
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL {
+3 -16
View File
@@ -6,16 +6,13 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
@class RACSignal;
// 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;
@@ -29,24 +26,14 @@
// on-disk locations for. This must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
// Finds or creates a storage folder.
// Finds or creates an Application Support folder.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)storageURL;
- (RACSignal *)applicationSupportURL;
// Determines where archived `SQRLShipItState` should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStateURL;
// Determines where ShipIt's stdout log should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStdoutURL;
// Determines where ShipIt's stderr log should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStderrURL;
@end
+12 -24
View File
@@ -7,8 +7,14 @@
//
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
@interface SQRLDirectoryManager ()
// The application identifier to use in file locations.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
@end
@implementation SQRLDirectoryManager
@@ -24,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.environment[@"FORCE_APP_IDENTIFIER"];
identifier = NSRunningApplication.currentApplication.localizedName;
}
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
@@ -47,11 +53,11 @@
#pragma mark Folder URLs
- (RACSignal *)storageURL {
- (RACSignal *)applicationSupportURL {
return [[RACSignal
defer:^{
NSError *error = nil;
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (folderURL == nil) {
return [RACSignal error:error];
}
@@ -63,36 +69,18 @@
return [RACSignal return:folderURL];
}]
setNameWithFormat:@"%@ -storageURL", self];
setNameWithFormat:@"%@ -applicationSupportURL", self];
}
- (RACSignal *)shipItStateURL {
return [[[self
storageURL]
applicationSupportURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipItState.plist"];
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
}
- (RACSignal *)shipItStdoutURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stdout.log"];
}]
setNameWithFormat:@"%@ -shipItStdoutURL", self];
}
- (RACSignal *)shipItStderrURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stderr.log"];
}]
setNameWithFormat:@"%@ -shipItStderrURL", self];
}
#pragma mark NSObject
- (NSString *)description {
+2 -2
View File
@@ -40,9 +40,9 @@
#pragma mark MTLJSONSerializing
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
};
}];
}
@end
-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 -16
View File
@@ -11,7 +11,7 @@
// The domain for errors originating within SQRLInstaller.
extern NSString * const SQRLInstallerErrorDomain;
// There was an error copying the target or update bundle to a backup location.
// There was an error copying the target bundle to the backup location.
extern const NSInteger SQRLInstallerErrorBackupFailed;
// There was an error replacing the target bundle with the update.
@@ -34,34 +34,25 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
// There was an error moving a bundle across volumes.
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
// There was an error changing the file permissions of the update.
extern const NSInteger SQRLInstallerErrorChangingPermissions;
@class RACCommand;
@class SQRLDirectoryManager;
// Performs the installation of an update, saving its intermediate state to user
// defaults.
// Performs the installation of an update, using the `SQRLShipItState` on disk,
// as located by `SQRLDirectoryManager`.
//
// This class is meant to be used only after the app that will be updated has
// terminated.
@interface SQRLInstaller : NSObject
// Initializes an installer using the given application identifier, which is
// used to scope resumable state stored to user defaults.
//
// applicationIdentifier - The defaults domain in which to store resumable
// state. Must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)applicationIdentifier;
// When executed with a `SQRLShipItRequest`, attempts to install the update or
// When executed with a `SQRLShipItState`, attempts to install the update or
// resume an in-progress installation.
//
// Each execution will complete or error on an unspecified scheduler when
// installation has completed or failed.
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
// When executed with a `SQRLShipItRequest`, aborts an installation, and
// attempts to restore the old version of the application if necessary.
// When executed with a `SQRLShipItState`, aborts an installation, and attempts
// to restore the old version of the application if necessary.
//
// This must not be executed while `installUpdateCommand` is executing.
//
@@ -69,4 +60,8 @@ extern const NSInteger SQRLInstallerErrorChangingPermissions;
// aborting/recovery has finished.
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
// Initializes an installer using the given directory manager to read and write the
// state of the installation.
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
@end
+355 -401
View File
@@ -7,25 +7,17 @@
//
#import "SQRLInstaller.h"
#import <libkern/OSAtomic.h>
#import "EXTScope.h"
#import <ReactiveCocoa/NSEnumerator+RACSequenceAdditions.h>
#import <ReactiveCocoa/NSObject+RACPropertySubscribing.h>
#import <ReactiveCocoa/RACCommand.h>
#import <ReactiveCocoa/RACSequence.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <sys/xattr.h>
#import "NSBundle+SQRLVersionExtensions.h"
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItRequest.h"
#import "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
#import "SQRLInstallerOwnedBundle.h"
#import <libkern/OSAtomic.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/xattr.h>
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
@@ -36,58 +28,85 @@ const NSInteger SQRLInstallerErrorInvalidBundleVersion = -4;
const NSInteger SQRLInstallerErrorMissingInstallationData = -5;
const NSInteger SQRLInstallerErrorInvalidState = -6;
const NSInteger SQRLInstallerErrorMovingAcrossVolumes = -7;
const NSInteger SQRLInstallerErrorChangingPermissions = -8;
NSString * const SQRLShipItInstallationAttemptsKey = @"SQRLShipItInstallationAttempts";
NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// Maps an installer state to a selector to invoke.
typedef struct {
// The state for which the associated method should be invoked.
SQRLInstallerState installerState;
// A method accepting a `SQRLShipItState` argument and returning a cold
// signal.
//
// If NULL, installation should complete.
SEL selector;
} SQRLInstallerDispatchTableEntry;
@interface SQRLInstaller ()
// The defaults domain to store all resumable state in.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
// Finds the state file to read and write from.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
// The bundle currently owned by this installer.
// Reads the given key from `state`, failing if it's not set.
//
// Stores the bundle moved aside by an install request so that the original
// bundle can be restored to its original location if needed.
@property (atomic, strong) SQRLInstallerOwnedBundle *ownedBundle;
// Reads the given key from `request`, failing if it's not set.
//
// key - The property key to read from `request`. This must not be nil, and
// should refer to a property of object type.
// request - The request object to read. This must not be nil.
// key - The property key to read from `state`. This must not be nil, and
// should refer to a property of object type.
// state - The state object to read. This must not be nil.
//
// Returns a signal which synchronously sends the non-nil read value then
// completes, or errors.
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request;
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state;
// Moves the updateBundleURL to an owned directory to prevent symlink attack,
// takes user:group ownership of the bundle, then verifies that it meets the
// designated requirement of the targetBundleURL.
// Performs the remaining stages of installation, as specified by `state`.
//
// request - The request whose update should be prepared and validated.
// state - The installation state. This must not be nil.
//
// Returns a signal which sends the owned & validated bundle URL then completes,
// or errors.
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request;
// Returns a signal which will complete or error on an unspecified thread.
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state;
// Saves a `SQRLInstallerOwnedBundle` for the targetBundleURL to the
// preferences, then moves the targetBundleURL to an owned directory.
// Moves the specified bundle to a backup location.
//
// request - The request whose target should be removed in preparation of an
// update being installed.
// bundleURL - The URL to the bundle that should be backed up. This must not be
// nil.
//
// Returns a signal which completes, or errors.
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request;
// Returns a signal which will send the `NSURL` to the proposed backup location
// as soon as possible, then complete once the bundle has actually been moved.
- (RACSignal *)backUpBundleAtURL:(NSURL *)bundleURL;
// Deletes a bundle that was moved into place using -moveAndTakeOwnershipOfBundleAtURL:.
// Deletes a bundle that was backed up using -backUpBundleAtURL:.
//
// bundleURL - The URL to the backup bundle, as sent from -moveAndTakeOwnershipOfBundleAtURL:.
// backupURL - The URL to the backup bundle, as sent from -backUpBundleAtURL:.
// This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL;
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL;
// Validates the code signature of a bundle, optionally restoring it upon
// failure.
//
// bundleURL - The URL of the bundle whose code signature should be
// verified. This must not be nil.
// signature - The code signature that the bundle must match. This must
// not be nil.
// backupBundleURL - If not nil, the URL to a bundle that should replace
// `bundleURL` if the code signature does not pass validation.
//
// Returns a signal which will synchronously complete if `bundleURL` passes
// validation, or if the bundle was recovered from `backupBundleURL`. If
// validation or recovery fails, an error will be sent.
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL;
// Attempts to determine whether a bundle has already been moved on disk.
//
// sourceURL - The original URL to the bundle. This must not be nil.
// targetURL - The proposed destination URL for the bundle. This must not be
// nil.
// signature - The code signature that any item must match in order to be
// considered the correct bundle. This must not be nil.
//
// Returns a signal which will synchronously send YES if `targetURL` points to
// a bundle matching the code signature, NO if it doesn't and `sourceURL` still
// exists, or an error otherwise.
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature;
// Moves `sourceURL` to `targetURL`.
//
@@ -99,7 +118,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// sourceURL - The URL to move from. This must not be nil.
//
// Retruns a signal which will synchronously complete or error.
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
// Recursively clears the quarantine extended attribute from the given
// directory.
@@ -113,28 +132,19 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// Returns a signal which will send completed or error on a background thread.
- (RACSignal *)clearQuarantineForDirectory:(NSURL *)directory;
// Recursively changes the owner and group of the given directory tree to that
// of the current process, then disables writing for anyone but the owner.
//
// directoryURL - The URL to the folder to take ownership of. This must not be
// nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL;
@end
@implementation SQRLInstaller
#pragma mark Lifecycle
- (id)initWithApplicationIdentifier:(NSString *)applicationIdentifier {
NSParameterAssert(applicationIdentifier != nil);
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
NSParameterAssert(directoryManager != nil);
self = [super init];
if (self == nil) return nil;
_applicationIdentifier = [applicationIdentifier copy];
_directoryManager = directoryManager;
@weakify(self);
@@ -146,63 +156,42 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
switchToLatest]
setNameWithFormat:@"aborting"];
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItRequest *request) {
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItState *state) {
@strongify(self);
NSParameterAssert(request != nil);
NSParameterAssert(state != nil);
// Request can be changed between launches, the installer may have
// already have an owned bundle, for a previous targetURL.
//
// If that's the case, we need to abort the previous owned bundle, and
// then handle the new install request.
return [[self
resumeInstallationFromState:state]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}];
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);
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItState *state) {
@strongify(self);
NSParameterAssert(state != nil);
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];
}]
catchTo:[RACSignal empty]]
then:^{
return [self installRequest:request];
}];
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];
}];
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItRequest *request) {
@strongify(self);
return [self abortInstall];
}];
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]];
@@ -210,184 +199,249 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
return [RACSignal return:value];
}
}]
setNameWithFormat:@"%@ -getRequiredKey: %@ fromRequest: %@", self, key, request];
setNameWithFormat:@"%@ -getRequiredKey: %@ fromState: %@", self, key, state];
}
#pragma mark Installer States
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
const SQRLInstallerDispatchTableEntry dispatchTablePrototype[] = {
{ .installerState = SQRLInstallerStateNothingToDo, .selector = NULL },
{ .installerState = SQRLInstallerStateClearingQuarantine, .selector = @selector(clearQuarantineWithState:) },
{ .installerState = SQRLInstallerStateBackingUp, .selector = @selector(backUpWithState:) },
{ .installerState = SQRLInstallerStateInstalling, .selector = @selector(installWithState:) },
{ .installerState = SQRLInstallerStateVerifyingInPlace, .selector = @selector(verifyInPlaceWithState:) },
};
return [[[[[[[self
ownedTemporaryDirectoryURL]
flattenMap:^(NSURL *directoryURL) {
return [self copyBundleAtURL:request.updateBundleURL toDirectory:directoryURL];
}]
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
NSValue *boxedDispatchTable = [NSValue valueWithBytes:dispatchTablePrototype objCType:@encode(__typeof__(dispatchTablePrototype))];
RACSignal *step = [RACSignal defer:^{
SQRLInstallerDispatchTableEntry dispatchTable[tableCount];
[boxedDispatchTable getValue:&dispatchTable];
SQRLInstallerState installerState = state.installerState;
size_t tableIndex;
for (tableIndex = 0; tableIndex < tableCount; tableIndex++) {
if (dispatchTable[tableIndex].installerState == installerState) {
break;
}
}
if (tableIndex >= tableCount) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Invalid installer state %i", nil), (int)installerState],
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
}
SEL selector = dispatchTable[tableIndex].selector;
if (selector == NULL) {
// Nothing to do.
return [RACSignal empty];
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
invocation.target = self;
invocation.selector = selector;
SQRLShipItState *stateArg = state;
[invocation setArgument:&stateArg atIndex:2];
[invocation invoke];
__unsafe_unretained RACSignal *step = nil;
[invocation getReturnValue:&step];
SQRLInstallerState nextState;
if (tableIndex + 1 >= tableCount) {
nextState = SQRLInstallerStateNothingToDo;
} else {
nextState = dispatchTable[tableIndex + 1].installerState;
}
return [[step
doCompleted:^{
NSLog(@"Completed state %i", (int)installerState);
}]
then:^{
return [RACSignal return:@(nextState)];
}];
}];
return [[self
stepRepeatedly:step withState:state]
setNameWithFormat:@"%@ -resumeInstallationFromState: %@", self, state];
}
- (RACSignal *)stepRepeatedly:(RACSignal *)step withState:(SQRLShipItState *)state {
NSParameterAssert(step != nil);
NSParameterAssert(state != nil);
return [step flattenMap:^(NSNumber *nextState) {
state.installerState = nextState.integerValue;
state.installationStateAttempt = 1;
return [[state
writeUsingURL:self.directoryManager.shipItStateURL]
// Automatically begin the next step.
concat:[self stepRepeatedly:step withState:state]];
}];
}
- (RACSignal *)clearQuarantineWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[self
getRequiredKey:@keypath(state.updateBundleURL) fromState:state]
flattenMap:^(NSURL *bundleURL) {
return [[[self
clearQuarantineForDirectory:bundleURL]
ignoreValues]
concat:[RACSignal return:bundleURL]];
return [self clearQuarantineForDirectory:bundleURL];
}]
zipWith:[self codeSignatureForBundleAtURL:request.targetBundleURL]]
reduceEach:^(NSURL *updateBundleURL, SQRLCodeSignature *codeSignature) {
return [[[self
verifyBundleAtURL:updateBundleURL usingSignature:codeSignature]
ignoreValues]
concat:[RACSignal return:updateBundleURL]];
}]
flatten]
setNameWithFormat:@"%@ -prepareAndValidateUpdateBundleURLForRequest: %@", self, request];
setNameWithFormat:@"%@ -clearQuarantineWithState: %@", self, state];
}
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
- (RACSignal *)backUpWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[[RACSignal
zip:@[
[self ownedTemporaryDirectoryURL],
[self codeSignatureForBundleAtURL:request.targetBundleURL],
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
NSURL *targetBundleURL = request.targetBundleURL;
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
}]
doNext:^(SQRLInstallerOwnedBundle *ownedBundle) {
self.ownedBundle = ownedBundle;
}]
flattenMap:^(SQRLInstallerOwnedBundle *ownedBundle) {
return [self installItemToURL:ownedBundle.temporaryURL fromURL:ownedBundle.originalURL];
}]
setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request];
}
- (RACSignal *)renameIfNeeded:(SQRLShipItRequest *)request updateBundleURL:(NSURL *)updateBundleURL {
if (!request.useUpdateBundleName) return [RACSignal return:request];
return [[self
renamedTargetIfNeededWithTargetURL:request.targetBundleURL sourceURL:updateBundleURL]
flattenMap:^(NSURL *newTargetURL) {
if ([newTargetURL isEqual:request.targetBundleURL]) return [RACSignal return:request];
SQRLShipItRequest *updatedRequest = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:newTargetURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:request.launchAfterInstallation useUpdateBundleName:request.useUpdateBundleName];
return [[self
installItemToURL:newTargetURL fromURL:request.targetBundleURL]
concat:[RACSignal return:updatedRequest]];
}];
}
- (RACSignal *)installRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
return [[[[self
prepareAndValidateUpdateBundleURLForRequest:request]
flattenMap:^(NSURL *updateBundleURL) {
return [[[[self
renameIfNeeded:request updateBundleURL:updateBundleURL]
flattenMap:^(SQRLShipItRequest *request) {
return [[self acquireTargetBundleURLForRequest:request] concat:[RACSignal return:request]];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [[[[[[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]];
}]
concat:[RACSignal return:request]];
}]
doCompleted:^{
self.ownedBundle = nil;
}];
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path]
setNameWithFormat:@"%@ -installRequest: %@", self, request];
}
- (RACSignal *)abortInstall {
// The request may have been tampered with to select a new targetURL to
// which the moved bundles should be restored.
//
// Discard the request parameters and restore the owned bundle to its
// original location.
SQRLInstallerOwnedBundle *ownedBundle = self.ownedBundle;
if (ownedBundle == nil) return [RACSignal empty];
return [[[[self
installItemToURL:ownedBundle.originalURL fromURL:ownedBundle.temporaryURL]
doCompleted:^{
self.ownedBundle = nil;
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), ownedBundle.originalURL.path]
setNameWithFormat:@"%@ -abortInstall", self];
}
#pragma mark Bundle Ownership
- (RACSignal *)ownedTemporaryDirectoryURL {
return [[[RACSignal
defer:^{
NSString *tmpPath = [NSTemporaryDirectory() stringByResolvingSymlinksInPath];
NSString *template = [NSString stringWithFormat:@"%@.XXXXXXXX", self.applicationIdentifier];
char *fullTemplate = strdup([tmpPath stringByAppendingPathComponent:template].UTF8String);
@onExit {
free(fullTemplate);
};
if (mkdtemp(fullTemplate) == NULL) {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state],
] reduce:^(NSURL *bundleURL, SQRLCodeSignature *codeSignature) {
RACSignal *skipBackup = [RACSignal return:@NO];
if (state.backupBundleURL != nil) {
skipBackup = [self checkWhetherBundlePreviouslyAtURL:bundleURL wasInstalledAtURL:state.backupBundleURL usingSignature:codeSignature];
}
NSURL *URL = [NSURL fileURLWithPath:[NSFileManager.defaultManager stringWithFileSystemRepresentation:fullTemplate length:strlen(fullTemplate)] isDirectory:YES];
return [RACSignal return:URL];
return [skipBackup flattenMap:^(NSNumber *skip) {
if (skip.boolValue) {
return [RACSignal empty];
} else {
return [self backUpBundleAtURL:bundleURL];
}
}];
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create temporary folder", nil)];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
flatten]
flattenMap:^(NSURL *backupBundleURL) {
// Save the chosen backup URL as soon as we have it, so we
// can resume even if the state change hasn't taken effect.
//
// N.B. It's important that this method remain
// synchronous, so it finishes before returning
// control to -backUpBundleAtURL:. Really, the flow
// here should be refactored so it doesn't matter.
state.backupBundleURL = backupBundleURL;
return [state writeUsingURL:self.directoryManager.shipItStateURL];
}]
setNameWithFormat:@"%@ -ownedDirectoryURL", self];
setNameWithFormat:@"%@ -backUpWithState: %@", self, state];
}
- (RACSignal *)copyBundleAtURL:(NSURL *)bundleURL toDirectory:(NSURL *)directoryURL {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(directoryURL != nil);
- (RACSignal *)installWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:bundleURL.lastPathComponent];
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.updateBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, NSURL *updateBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
return [[[[self
checkWhetherBundlePreviouslyAtURL:updateBundleURL wasInstalledAtURL:targetBundleURL usingSignature:codeSignature]
flattenMap:^(NSNumber *skip) {
if (skip.boolValue) {
return [RACSignal empty];
} else {
return [self installItemAtURL:targetBundleURL fromURL:updateBundleURL];
}
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to replace bundle %@ with update %@", nil), targetBundleURL, updateBundleURL];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorReplacingTarget toError:error]];
}]
catch:^(NSError *error) {
// Verify that the target bundle didn't get corrupted during
// failure. Try recovering it if it did.
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
then:^{
// Recovery succeeded, but we still want to pass
// through the original error.
return [RACSignal error:error];
}];
}];
}]
flatten]
setNameWithFormat:@"%@ -installWithState: %@", self, state];
}
- (RACSignal *)verifyInPlaceWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
then:^{
return [[self
deleteBackupAtURL:backupBundleURL]
catchTo:[RACSignal empty]];
}];
}]
flatten]
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
}
#pragma mark Backing Up
- (RACSignal *)backUpBundleAtURL:(NSURL *)targetBundleURL {
NSParameterAssert(targetBundleURL != nil);
return [[[[[[self.directoryManager
applicationSupportURL]
flattenMap:^(NSURL *applicationSupportURL) {
NSError *error = nil;
NSURL *temporaryDirectoryURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:applicationSupportURL create:YES error:&error];
if (temporaryDirectoryURL == nil) {
return [RACSignal error:error];
}
return [RACSignal return:temporaryDirectoryURL];
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create backup folder", nil)];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
map:^(NSURL *temporaryDirectoryURL) {
return [temporaryDirectoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
}]
flattenMap:^(NSURL *backupBundleURL) {
return [[[[self
installItemAtURL:backupBundleURL fromURL:targetBundleURL]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to move bundle %@ to backup location %@", nil), targetBundleURL, backupBundleURL];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
ignoreValues]
// Return the backup URL before doing any work, to increase
// fault tolerance.
startWith:backupBundleURL];
}]
setNameWithFormat:@"%@ -backUpBundleAtURL: %@", self, targetBundleURL];
}
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
NSParameterAssert(backupURL != nil);
return [[[RACSignal
defer:^{
NSError *error;
BOOL copy = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:newBundleURL error:&error];
if (!copy) return [RACSignal error:error];
return [RACSignal return:newBundleURL];
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to copy bundle %@ to directory %@", nil), bundleURL, newBundleURL];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
setNameWithFormat:@"%@ -copyBundleAtURL: %@ toDirectory: %@", self, bundleURL, directoryURL];
}
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
return [[[RACSignal
defer:^{
NSError *error;
if ([NSFileManager.defaultManager removeItemAtURL:bundleURL error:&error]) {
NSError *error = nil;
if ([NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
return [RACSignal empty];
} else {
return [RACSignal error:error];
@@ -395,7 +449,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
}]
then:^{
// Also remove the temporary directory that the backup lived in.
NSURL *temporaryDirectoryURL = bundleURL.URLByDeletingLastPathComponent;
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
// However, use rmdir() to skip it in case there are other files
// contained within (for whatever reason).
@@ -405,67 +459,58 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
}]
setNameWithFormat:@"%@ -deleteOwnedBundleAtURL: %@", self, bundleURL];
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
}
#pragma mark Verification
- (RACSignal *)codeSignatureForBundleAtURL:(NSURL *)URL {
return [[RACSignal
defer:^{
NSError *error;
SQRLCodeSignature *codeSignature = [SQRLCodeSignature signatureWithBundle:URL error:&error];
if (codeSignature == nil) return [RACSignal error:error];
return [RACSignal return:codeSignature];
}]
setNameWithFormat:@"%@ -codeSignatureForBundleAtURL: %@", self, URL];
}
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature {
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(signature != nil);
return [[[self
takeOwnershipOfDirectory:bundleURL]
then:^{
return [signature verifyBundleAtURL:bundleURL];
return [[[signature
verifyBundleAtURL:bundleURL]
catch:^(NSError *error) {
if (backupBundleURL == nil) return [RACSignal error:error];
return [[[[self
installItemAtURL:bundleURL fromURL:backupBundleURL]
initially:^{
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
}]
doCompleted:^{
NSLog(@"Restored backup bundle to %@", bundleURL);
}]
doError:^(NSError *recoveryError) {
NSLog(@"Could not restore backup bundle %@ to %@: %@", backupBundleURL, bundleURL, recoveryError.sqrl_verboseDescription);
}];
}]
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@", self, bundleURL, signature];
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@ recoveringUsingBackupAtURL: %@", self, bundleURL, signature, backupBundleURL];
}
#pragma mark Installation
/// Check if the target should be renamed and provide the renamed URL.
///
/// targetURL - The URL for the target. Cannot be nil.
/// sourceURL - The URL for the source. Cannot be nil.
///
/// Returns a signal which will send the URL for the renamed target. If a rename
/// isn't needed then it will send `targetURL`.
- (RACSignal *)renamedTargetIfNeededWithTargetURL:(NSURL *)targetURL sourceURL:(NSURL *)sourceURL {
return [RACSignal defer:^{
NSBundle *sourceBundle = [NSBundle bundleWithURL:sourceURL];
NSString *targetExecutableName = targetURL.lastPathComponent.stringByDeletingPathExtension;
NSString *sourceExecutableName = sourceBundle.sqrl_executableName;
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
NSParameterAssert(signature != nil);
// If they're already the same then we're good.
if ([targetExecutableName isEqual:sourceExecutableName]) {
return [RACSignal return:targetURL];
}
NSString *newAppName = [sourceExecutableName stringByAppendingPathExtension:@"app"];
NSURL *newTargetURL = [targetURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:newAppName];
// If there's already something there then don't rename to it.
if ([NSFileManager.defaultManager fileExistsAtPath:newTargetURL.path]) {
return [RACSignal return:targetURL];
}
return [RACSignal return:newTargetURL];
}];
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 *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
@@ -477,7 +522,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
} else {
int code = errno;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
@@ -529,7 +574,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// file to begin with.
if (code != ENOATTR) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
@@ -542,97 +587,6 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
setNameWithFormat:@"%@ -clearQuarantineForDirectory: %@", self, directory];
}
#pragma mark File Security
- (RACSignal *)readFileSecurityOfURL:(NSURL *)location {
NSParameterAssert(location != nil);
return [[RACSignal
defer:^{
NSError *error;
NSFileSecurity *fileSecurity;
if (![location getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
return [RACSignal error:error];
}
return [RACSignal return:fileSecurity];
}]
setNameWithFormat:@"%@ -readFileSecurity: %@", self, location];
}
- (RACSignal *)writeFileSecurity:(NSFileSecurity *)fileSecurity toURL:(NSURL *)location {
NSParameterAssert(location != nil);
return [[RACSignal
defer:^{
NSError *error;
if (![location setResourceValue:fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -writeFileSecurity: %@", self, location];
}
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL {
NSParameterAssert(directoryURL != nil);
return [[[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
NSDirectoryEnumerator *enumerator = [NSFileManager.defaultManager enumeratorAtURL:directoryURL includingPropertiesForKeys:@[ NSURLFileSecurityKey ] options:0 errorHandler:^ BOOL (NSURL *url, NSError *error) {
[subscriber sendError:error];
return NO;
}];
return [[enumerator.rac_sequence.signal
startWith:directoryURL]
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 {
-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 "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
+10 -5
View File
@@ -22,7 +22,8 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
@implementation SQRLShipItLauncher
+ (NSString *)shipItJobLabel {
return [SQRLDirectoryManager.currentApplicationManager.applicationIdentifier stringByAppendingString:@".ShipIt"];
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
}
+ (RACSignal *)shipItJobDictionary {
@@ -31,9 +32,9 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
return [RACSignal zip:@[ [directoryManager shipItStdoutURL], [directoryManager shipItStderrURL] ]];
return [directoryManager applicationSupportURL];
}]
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL) {
map:^(NSURL *appSupportURL) {
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
@@ -57,9 +58,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
[arguments addObject:jobLabel];
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = arguments;
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = stdoutURL.path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = stderrURL.path;
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"];
-100
View File
@@ -1,100 +0,0 @@
//
// SQRLShipItRequest.h
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class RACSignal;
// The domain for errors originating within `SQRLShipItRequest`.
extern NSString * const SQRLShipItRequestErrorDomain;
// Errors originating from the `SQRLShipItRequestErrorDomain`.
//
// SQRLShipItRequestErrorMissingRequiredProperty - A required property was `nil`
// upon initialization.
//
// The `userInfo` dictionary for
// this error will contain
// `SQRLShipItStatePropertyErrorKey`.
//
// SQRLShipItRequestErrorUnarchiving - The saved request could not
// be unarchived, possibly
// because it's invalid.
//
// SQRLShipItRequestErrorArchiving - The request object could not
// be archived.
typedef enum : NSInteger {
SQRLShipItRequestErrorMissingRequiredProperty = 1,
SQRLShipItRequestErrorUnarchiving = 2,
SQRLShipItRequestErrorArchiving = 3,
} SQRLShipItRequestError;
// Associated with an `NSString` indicating the required property key that did
// not have a value upon initialization.
extern NSString * const SQRLShipItRequestPropertyErrorKey;
// Constructed and written to disk for `ShipIt` to pick up. This represents a
// single update request from the client's perspective.
@interface SQRLShipItRequest : MTLModel
// Reads a `SQRLShipItState` from disk, at the location specified by the given
// URL signal.
//
// URLSignal - Determines the file location to read from, the signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
// complete, or error.
+ (RACSignal *)readUsingURL:(RACSignal *)URLSignal;
// Writes the receiver to disk, at the location specified by the given URL
// signal.
//
// URLSignal - Determines the file location to write to. The signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeUsingURL:(RACSignal *)URLSignal;
// Designated initialiser.
//
// updateBundleURL - The update bundle which will replace
// targetBundleURL. Must not be nil.
// targetBundleURL - Where the update should be installed, if a bundle
// is already present, the update is checked for
// suitability against this bundle. Must not be nil.
// bundleIdentifier - The bundle identifier that the installer should
// wait for instances of to terminate before
// installing. Can be nil.
// launchAfterInstallation - Whether the updated application should be launched
// after installation.
// useUpdateBundleName - Should the target use the update bundle's name?
//
// 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 useUpdateBundleName:(BOOL)useUpdateBundleName;
// 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;
// Whether the app should use the update bundle's name.
@property (nonatomic, assign, readonly) BOOL useUpdateBundleName;
@end
-194
View File
@@ -1,194 +0,0 @@
//
// SQRLShipItRequest.m
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLShipItRequest.h"
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/RACSignal+Operations.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 useUpdateBundleName:(BOOL)useUpdateBundleName {
return [self initWithDictionary:@{
@keypath(self.updateBundleURL): updateBundleURL,
@keypath(self.targetBundleURL): targetBundleURL,
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
@keypath(self.launchAfterInstallation): @(launchAfterInstallation),
@keypath(self.useUpdateBundleName): @(useUpdateBundleName),
} error:NULL];
}
#pragma mark Serialization
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{};
}
+ (NSValueTransformer *)updateBundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)targetBundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[[URL
flattenMap:^(NSURL *stateURL) {
NSError *error;
NSData *data = [self readFromURL:stateURL error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
flattenMap:^(NSData *data) {
return [self readFromData:data];
}]
catch:^(NSError *error) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read update request", nil),
};
if (error != nil) {
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
NSUnderlyingErrorKey: error,
}];
}
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorUnarchiving userInfo:userInfo]];
}]
setNameWithFormat:@"+readUsingURL: %@", URL];
}
+ (NSData *)readFromURL:(NSURL *)URL error:(NSError **)errorRef {
__block NSData *data = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateReadingItemAtURL:URL options:NSFileCoordinatorReadingWithoutChanges error:errorRef byAccessor:^(NSURL *newURL) {
data = [NSData dataWithContentsOfURL:newURL options:NSDataReadingUncached error:errorRef];
}];
return data;
}
+ (RACSignal *)readFromData:(NSData *)data {
return [[RACSignal
defer:^{
NSError *error;
NSDictionary *JSONDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (JSONDictionary == nil) {
return [RACSignal error:error];
}
SQRLShipItRequest *request = [MTLJSONAdapter modelOfClass:SQRLShipItRequest.class fromJSONDictionary:JSONDictionary error:&error];
if (request == nil) {
return [RACSignal error:error];
}
return [RACSignal return:request];
}]
setNameWithFormat:@"+readFromData: <NSData %p>", data];
}
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[[RACSignal
zip:@[
URL,
[self serialization]
] reduce:^(NSURL *stateURL, NSData *data) {
NSError *error;
BOOL write = [self writeData:data toURL:stateURL error:&error];
if (!write) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
flatten]
catch:^(NSError *error) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not write update request", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil),
};
if (error != nil) {
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
NSUnderlyingErrorKey: error,
}];
}
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorArchiving userInfo:userInfo]];
}]
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
}
- (BOOL)writeData:(NSData *)data toURL:(NSURL *)URL error:(NSError **)errorRef {
__block BOOL success = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateWritingItemAtURL:URL options:0 error:errorRef byAccessor:^(NSURL *newURL) {
success = [data writeToURL:newURL options:NSDataWritingAtomic error:errorRef];
}];
return success;
}
- (RACSignal *)serialization {
return [[RACSignal
defer:^{
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:self];
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
setNameWithFormat:@"%@ serialization", self];
}
@end
+117
View File
@@ -0,0 +1,117 @@
//
// SQRLShipItState.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-08.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
// The domain for errors originating within `SQRLShipItState`.
extern NSString * const SQRLShipItStateErrorDomain;
// A required property was `nil` upon initialization.
//
// The `userInfo` dictionary for this error will contain
// `SQRLShipItStatePropertyErrorKey`.
extern const NSInteger SQRLShipItStateErrorMissingRequiredProperty;
// The saved state on disk could not be unarchived, possibly because it's
// invalid.
extern const NSInteger SQRLShipItStateErrorUnarchiving;
// The state object could not be archived.
extern const NSInteger SQRLShipItStateErrorArchiving;
// Associated with an NSString indicating the required property key that did not
// have a value upon initialization.
extern NSString * const SQRLShipItStatePropertyErrorKey;
// The current state of the installer, for persistence across relaunches and for
// tolerance of system failures.
//
// SQRLInstallerStateNothingToDo - ShipIt has not started installing yet.
// SQRLInstallerStateClearingQuarantine - Clearing the quarantine flag on the
// update bundle so it can used without
// issue.
// SQRLInstallerStateBackingUp - Backing up the target bundle so it can
// be restored in the event of failure.
// SQRLInstallerStateInstalling - Replacing the target bundle with the
// update bundle.
// SQRLInstallerStateVerifyingInPlace - Verifying that the target bundle is
// still valid after updating.
//
// Note that these values must remain backwards compatible, so ShipIt doesn't
// start up in a weird mode on a newer version.
typedef enum : NSInteger {
SQRLInstallerStateNothingToDo = 0,
SQRLInstallerStateClearingQuarantine,
SQRLInstallerStateBackingUp,
SQRLInstallerStateInstalling,
SQRLInstallerStateVerifyingInPlace,
} SQRLInstallerState;
@class RACSignal;
@class SQRLCodeSignature;
// Encapsulates all the state needed by the ShipIt process.
@interface SQRLShipItState : MTLModel
// Reads a `SQRLShipItState` from disk, at the location specified by the given
// URL signal.
//
// URLSignal - Determines the file location to read from, the signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItState` then
// complete, or error.
+ (RACSignal *)readUsingURL:(RACSignal *)URL;
// Initializes the receiver with the arguments that will not change during
// installation.
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature;
// Writes the receiver to disk, at the location specified by the given URL
// signal.
//
// URL - Determines the file location to write to. The signal should send an
// `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeUsingURL:(RACSignal *)URL;
// The URL to the app bundle that should be replaced with an update.
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
// The URL to the downloaded update's app bundle.
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
// A code signature that the update bundle must match in order to be valid.
@property (nonatomic, copy, readonly) SQRLCodeSignature *codeSignature;
// The bundle identifier of the application being updated.
//
// If not nil, the installer will wait for applications matching this identifier
// (and `targetBundleURL`) to terminate before continuing.
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
// The current state of the installer.
@property (atomic, assign) SQRLInstallerState installerState;
// The number of installation attempts that have occurred for the current
// `state`.
@property (atomic, assign) NSUInteger installationStateAttempt;
// Whether to relaunch the application after an update is successfully
// installed.
@property (atomic, assign) BOOL relaunchAfterInstallation;
// The URL where the target bundle has been backed up to before installing the
// update.
//
// This property is set automatically during the course of installation. It
// should not be preset.
@property (atomic, copy) NSURL *backupBundleURL;
@end
+122
View File
@@ -0,0 +1,122 @@
//
// SQRLShipItState.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-08.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLShipItState.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
NSString * const SQRLShipItStateErrorDomain = @"SQRLShipItStateErrorDomain";
NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErrorKey";
const NSInteger SQRLShipItStateErrorMissingRequiredProperty = 1;
const NSInteger SQRLShipItStateErrorUnarchiving = 2;
const NSInteger SQRLShipItStateErrorArchiving = 3;
@implementation SQRLShipItState
#pragma mark Lifecycle
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [super initWithDictionary:dictionary error:error];
if (self == nil) return nil;
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
if ([self valueForKey:key] != nil) return YES;
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
};
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
}
return NO;
};
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
if (!validateKey(@keypath(self.codeSignature))) return nil;
return self;
}
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature {
return [self initWithDictionary:@{
@keypath(self.targetBundleURL): targetBundleURL,
@keypath(self.updateBundleURL): updateBundleURL,
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
@keypath(self.codeSignature): codeSignature,
} error:NULL];
}
#pragma mark Serialization
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[URL
flattenMap:^(NSURL *stateURL) {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:stateURL options:NSDataReadingUncached error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
flattenMap:^(NSData *data) {
SQRLShipItState *state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (![state isKindOfClass:SQRLShipItState.class]) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read saved state", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
}
return [RACSignal return:state];
}]
setNameWithFormat:@"+readUsingURL: %@", URL];
}
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
RACSignal *serialization = [RACSignal defer:^{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self];
if (data == nil) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not save state", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
}
return [RACSignal return:data];
}];
return [[[RACSignal
zip:@[
URL,
serialization
] reduce:^(NSURL *stateURL, NSData *data) {
NSError *error = nil;
if (![data writeToURL:stateURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
flatten]
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
}
@end
+1 -1
View File
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
@class RACSignal;
+2 -8
View File
@@ -7,14 +7,7 @@
//
#import "SQRLTerminationListener.h"
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/NSArray+RACSequenceAdditions.h>
#import <ReactiveCocoa/RACDisposable.h>
#import <ReactiveCocoa/RACScheduler.h>
#import <ReactiveCocoa/RACSequence.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLTerminationListener ()
@@ -81,6 +74,7 @@
dispatch_resume(source);
return [RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
dispatch_release(source);
}];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
+1 -1
View File
@@ -74,7 +74,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
+ (NSValueTransformer *)releaseDateJSONTransformer {
// ISO 8601 Time Zone with ':'
NSString * const ISO8601DateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZZZ";
NSString * const ISO8601DateFormat = @"yyyy'-'MM'-'DD'T'HH':'mm':'ssZZZZZ";
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^ NSDate * (NSString *dateString) {
if (![dateString isKindOfClass:NSString.class]) return nil;
-22
View File
@@ -6,23 +6,6 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
// 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;
@@ -72,11 +55,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// If an update is available, it will be sent on `updates` once downloaded.
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
// The current state of the manager.
//
// This property is KVO-compliant.
@property (atomic, readonly) SQRLUpdaterState state;
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
// update is available.
//
+92 -130
View File
@@ -15,9 +15,9 @@
#import "SQRLDirectoryManager.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLUpdate.h"
#import "SQRLZipArchiver.h"
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@@ -38,12 +38,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@interface SQRLUpdater ()
@property (atomic, readwrite) SQRLUpdaterState state;
/// The etag of the currently downloaded update, nil if no update has been
/// downloaded.
@property (atomic, copy) NSString *etag;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
@@ -53,6 +47,16 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// Sends completed or error.
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
// Lazily removes outdated temporary directories (used for previous updates)
// 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;
// Parses an update model from downloaded data.
//
// data - JSON data representing an update manifest. This must not be nil.
@@ -120,6 +124,16 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// Returns a signal which completes or errors on a background thread.
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update;
// Verifies that an existing state is innocuous, and therefore safe to
// overwrite.
//
// This won't be the case if, for example, an update is currently being
// installed.
//
// Returns a signal which sends `existingState` then completes upon successful
// validation, or errors otherwise.
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState;
@end
@implementation SQRLUpdater
@@ -163,6 +177,33 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_prunedUpdateDirectories = [[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.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);
@@ -171,11 +212,13 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[[[self
performHousekeeping]
// Prune old updates before the first update check.
return [[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
then:^{
self.state = SQRLUpdaterStateCheckingForUpdate;
return [NSURLConnection rac_sendAsynchronousRequest:request];
}]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
@@ -203,19 +246,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [[RACSignal
defer:^{
self.state = SQRLUpdaterStateDownloadingUpdate;
return [self downloadAndPrepareUpdate:update];
}]
doCompleted:^{
self.state = SQRLUpdaterStateAwaitingRelaunch;
}];
}]
finally:^{
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return;
self.state = SQRLUpdaterStateIdle;
return [self downloadAndPrepareUpdate:update];
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
@@ -328,20 +359,12 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSURL *zipDownloadURL = update.updateURL;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
if (self.etag != nil) {
[zipDownloadRequest setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
return [[[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (httpResponse.statusCode == 304 /* Not Modified */) {
return [RACSignal empty];
}
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
@@ -351,8 +374,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
self.etag = httpResponse.allHeaderFields[@"ETag"];
}
return [RACSignal return:bodyData];
@@ -394,10 +415,10 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -475,84 +496,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
setNameWithFormat:@"%@ -shipItStateURL", self];
}
- (RACSignal *)performHousekeeping {
return [[RACSignal
merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
catch:^(NSError *error) {
NSLog(@"Error doing housekeeping: %@", error);
return [RACSignal empty];
}];
}
/// Lazily removes outdated temporary directories (used for previous updates)
/// upon 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.
- (RACSignal *)pruneUpdateDirectories {
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *storageURL) {
NSFileManager *manager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, 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);
}
}];
}]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
}
// Like truncation, but backwards.
- (RACSignal *)backwardTruncateFile:(NSURL *)fileURL {
return [RACSignal defer:^{
static const NSInteger MAX_LENGTH = 1024 * 1024 * 8;
NSError *error;
NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:fileURL error:&error];
if (handle == nil) return [RACSignal error:error];
unsigned long long fileLength = [handle seekToEndOfFile];
if (fileLength <= MAX_LENGTH) return [RACSignal empty];
[handle seekToFileOffset:fileLength - MAX_LENGTH];
NSData *mostRecentData = [handle readDataToEndOfFile];
[handle truncateFileAtOffset:0];
[handle writeData:mostRecentData];
return [RACSignal empty];
}];
}
- (RACSignal *)truncateLogs {
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [RACSignal zip:@[ [directoryManager shipItStdoutURL], [directoryManager shipItStderrURL] ]];
}]
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL) {
return [RACSignal merge:@[ [self backwardTruncateFile:stdoutURL], [self backwardTruncateFile:stderrURL] ]];
}];
}
#pragma mark Installing Updates
- (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle *)updateBundle {
@@ -573,19 +516,37 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
setNameWithFormat:@"%@ -verifyAndPrepareUpdate: %@ fromBundle: %@", self, update, updateBundle];
}
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState {
return [[RACSignal
defer:^{
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
// If this happens, shit is crazy, because it implies that an
// update is being installed over us right now.
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal return:existingState];
}]
setNameWithFormat:@"%@ -validateExistingState: %@", self, existingState];
}
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update {
NSParameterAssert(update != nil);
return [[[[RACSignal
defer:^{
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
NSBundle *appBundle = [NSBundle bundleWithURL:currentApplication.bundleURL];
// Only use the update bundle's name if the user hasn't renamed the
// app themselves.
BOOL useUpdateBundleName = [appBundle.sqrl_executableName isEqual:currentApplication.bundleURL.lastPathComponent.stringByDeletingPathExtension];
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO useUpdateBundleName:useUpdateBundleName];
return [request writeUsingURL:self.shipItStateURL];
return [[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
then:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
return [state writeUsingURL:self.shipItStateURL];
}]
then:^{
return self.shipItLauncher;
@@ -595,13 +556,14 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[[SQRLShipItRequest
return [[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
map:^(SQRLShipItRequest *request) {
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES useUpdateBundleName:request.useUpdateBundleName];
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [[request
flattenMap:^(SQRLShipItState *state) {
state.relaunchAfterInstallation = YES;
return [[state
writeUsingURL:self.shipItStateURL]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
}]
+98 -112
View File
@@ -7,19 +7,15 @@
//
#import <Foundation/Foundation.h>
#import "EXTScope.h"
#import <ReactiveCocoa/RACCommand.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACScheduler.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 "SQRLInstaller+Private.h"
#import "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
#import "SQRLShipItRequest.h"
// The maximum number of times ShipIt should run the same installation state, in
// an attempt to update.
@@ -31,117 +27,19 @@ static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
static NSUInteger installationAttempts(NSString *applicationIdentifier) {
return CFPreferencesGetAppIntegerValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFStringRef)applicationIdentifier, NULL);
}
static BOOL setInstallationAttempts(NSString *applicationIdentifier, NSUInteger attempts) {
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(attempts), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
}
static BOOL clearInstallationAttempts(NSString *applicationIdentifier) {
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, NULL, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
}
// Waits for all instances of the target application (as described in the
// `request`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItRequest *request) {
// `state`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
return [[RACSignal
defer:^{
if (request.bundleIdentifier == nil) return [RACSignal empty];
if (state.bundleIdentifier == nil) return [RACSignal empty];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
return [listener waitForTermination];
}]
setNameWithFormat:@"waitForTerminationIfNecessary"];
}
static void installRequest(RACSignal *readRequestSignal, SQRLDirectoryManager *directoryManager) {
NSString *applicationIdentifier = directoryManager.applicationIdentifier;
[[[[[readRequestSignal
flattenMap:^(SQRLShipItRequest *request) {
return waitForTerminationIfNecessary(request);
}]
ignoreValues]
concat:readRequestSignal]
flattenMap:^(SQRLShipItRequest *request) {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
NSUInteger attempt = installationAttempts(applicationIdentifier) + 1;
setInstallationAttempts(applicationIdentifier, attempt);
RACSignal *action;
if (attempt > SQRLShipItMaximumInstallationAttempts) {
action = [[[[installer.abortInstallationCommand
execute:request]
initially:^{
NSLog(@"Too many attempts to install, aborting update");
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
// Exit successfully so launchd doesn't restart us
// again.
return [RACSignal empty];
}]
concat:[RACSignal return:request]];
} else {
action = [[[[installer.installUpdateCommand
execute:request]
initially:^{
BOOL freshInstall = (attempt == 1);
if (freshInstall) {
NSLog(@"Beginning installation");
} else {
NSLog(@"Resuming installation attempt %i", (int)attempt);
}
}]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path];
}
// Clear the installation attempts for a successful abort or
// install.
action = [action doCompleted:^{
clearInstallationAttempts(applicationIdentifier);
}];
if (request.launchAfterInstallation) {
// Launch regardless of whether installation succeeds or fails.
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
doNext:^(SQRLShipItRequest *finalRequest) {
NSURL *bundleURL = finalRequest.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot launch application");
return;
}
NSError *error;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:@{} error:&error]) {
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application launched at %@", bundleURL);
}];
}
return action;
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
@@ -153,13 +51,101 @@ int main(int argc, const char * argv[]) {
return EXIT_FAILURE;
}
char const *jobLabel = argv[1];
const char *jobLabel = argv[1];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
RACSignal *stateLocation = directoryManager.shipItStateURL;
installRequest([SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL], directoryManager);
[[[[[[SQRLShipItState
readUsingURL:stateLocation]
flattenMap:^(SQRLShipItState *state) {
return waitForTerminationIfNecessary(state);
}]
then:^{
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
return [SQRLShipItState readUsingURL:stateLocation];
}]
catch:^(NSError *error) {
NSLog(@"Error reading saved installer state: %@", error);
// Exit successfully so launchd doesn't restart us again.
return [RACSignal empty];
}]
flattenMap:^(SQRLShipItState *state) {
BOOL freshInstall = (state.installerState == SQRLInstallerStateNothingToDo);
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:directoryManager];
NSUInteger attempt = (freshInstall ? 1 : state.installationStateAttempt + 1);
RACSignal *action;
if (attempt > SQRLShipItMaximumInstallationAttempts) {
action = [[[installer.abortInstallationCommand
execute:state]
initially:^{
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
// Exit successfully so launchd doesn't restart us again.
return [RACSignal empty];
}];
} else {
action = [[[[[state
writeUsingURL:stateLocation]
initially:^{
if (freshInstall) {
NSLog(@"Beginning installation");
state.installerState = SQRLInstallerStateClearingQuarantine;
} else {
NSLog(@"Resuming installation from state %i", (int)state.installerState);
}
state.installationStateAttempt = attempt;
}]
then:^{
return [installer.installUpdateCommand execute:state];
}]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
if (state.relaunchAfterInstallation) {
// Relaunch regardless of whether installation succeeds or
// fails.
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
finally:^{
NSURL *bundleURL = state.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot relaunch application");
return;
}
NSError *error = nil;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
NSLog(@"Could not relaunch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application relaunched at %@", bundleURL);
}];
}
return action;
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
dispatch_main();
}
return EXIT_SUCCESS;
}
+1 -1
View File
@@ -9,7 +9,7 @@
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
+8
View File
@@ -0,0 +1,8 @@
//
// Prefix header for all source files of the 'Squirrel' target in the 'Squirrel' project
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SystemConfiguration/SystemConfiguration.h>
#endif
-8
View File
@@ -6,14 +6,6 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Squirrel.
FOUNDATION_EXPORT double SquirrelVersionNumber;
//! Project version string for Squirrel.
FOUNDATION_EXPORT const unsigned char SquirrelVersionString[];
#import <Squirrel/NSBundle+SQRLVersionExtensions.h>
#import <Squirrel/NSProcessInfo+SQRLVersionExtensions.h>
#import <Squirrel/SQRLDownloadedUpdate.h>
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
+7 -12
View File
@@ -6,35 +6,30 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(NSProcessInfoExtensions)
SpecBegin(NSProcessInfoExtensions)
describe(@"-sqrl_operatingSystemShortVersionString", ^{
__block NSString *versionString;
beforeEach(^{
versionString = NSProcessInfo.processInfo.sqrl_operatingSystemShortVersionString;
expect(versionString).notTo(beNil());
expect(versionString).notTo.beNil();
});
it(@"should follow major.minor.patch format", ^{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[0-9]+(\\.[0-9]+)*$" options:0 error:&error];
expect(regex).notTo(beNil());
expect(error).to(beNil());
expect(regex).notTo.beNil();
expect(error).to.beNil();
NSRange fullRange = NSMakeRange(0, versionString.length);
NSRange matchRange = [regex rangeOfFirstMatchInString:versionString options:0 range:fullRange];
expect(@(NSEqualRanges(matchRange, fullRange))).to(beTruthy());
expect(NSEqualRanges(matchRange, fullRange)).to.beTruthy();
});
it(@"should be numerically comparable", ^{
expect(@([@"10.6.9" compare:versionString options:NSNumericSearch])).to(equal(@(NSOrderedAscending)));
expect([@"10.6.9" compare:versionString options:NSNumericSearch]).to.equal(NSOrderedAscending);
});
});
QuickSpecEnd
SpecEnd
@@ -6,16 +6,9 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLCodeSignature.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLCodeSignatureSpec)
SpecBegin(SQRLCodeSignature)
__block NSBundle *bundle;
__block void (^corruptURL)(NSURL *URL);
@@ -24,26 +17,26 @@ beforeEach(^{
bundle = self.testApplicationBundle;
corruptURL = ^(NSURL *URL) {
expect(@([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL])).to(beTruthy());
expect([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL]).to.beTruthy();
};
});
it(@"should verify a valid bundle", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should fail to verify with different code signing requirements", ^{
NSError *error = nil;
SQRLCodeSignature *signature = [SQRLCodeSignature currentApplicationSignature:&error];
expect(signature).notTo(beNil());
expect(error).to(beNil());
SQRLCodeSignature *verifier = [SQRLCodeSignature currentApplicationSignature:&error];
expect(verifier).notTo.beNil();
expect(error).to.beNil();
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(error).notTo(beNil());
BOOL success = [[verifier verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
});
describe(@"code signature changes", ^{
@@ -54,15 +47,15 @@ describe(@"code signature changes", ^{
});
it(@"should fail to verify a bundle with a missing code signature", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt code signature", ^{
@@ -70,25 +63,25 @@ describe(@"code signature changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
describe(@"main executable changes", ^{
it(@"should fail to verify a bundle with a missing executable", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorCouldNotCreateStaticCode)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
});
it(@"should fail to verify a bundle with a corrupt executable", ^{
@@ -96,11 +89,11 @@ describe(@"main executable changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -109,19 +102,19 @@ describe(@"helper executable changes", ^{
beforeEach(^{
helperURL = [bundle URLForAuxiliaryExecutable:@"unused-helper"];
expect(helperURL).notTo(beNil());
expect(helperURL).notTo.beNil();
});
it(@"should fail to verify a bundle with a missing helper", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:helperURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:helperURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt helper", ^{
@@ -129,11 +122,11 @@ describe(@"helper executable changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -142,19 +135,19 @@ describe(@"resource changes", ^{
beforeEach(^{
resourceURL = [bundle URLForResource:@"MainMenu" withExtension:@"nib"];
expect(resourceURL).notTo(beNil());
expect(resourceURL).notTo.beNil();
});
it(@"should fail to verify a bundle with a missing resource", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:resourceURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:resourceURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt resource", ^{
@@ -162,11 +155,11 @@ describe(@"resource changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -178,15 +171,15 @@ describe(@"framework changes", ^{
});
it(@"should fail to verify a bundle with a missing framework", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:frameworkURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:frameworkURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt framework", ^{
@@ -194,12 +187,12 @@ describe(@"framework changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
QuickSpecEnd
SpecEnd
+119
View File
@@ -0,0 +1,119 @@
//
// SQRLDeepCodesignSpec.m
// Squirrel
//
// Created by Keith Duncan on 2013-09-05.
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLDeepCodesign)
NSMutableDictionary * (^environmentSuitableForChildProcess)(void) = ^ {
// Remove environment variables that configure the Obj-C runtime
// Specifically OBJC_DISABLE_GC so that child processes aren't forced to
// adopt the GC preference of the test suite
NSMutableDictionary *environment = [NSProcessInfo.processInfo.environment mutableCopy];
NSSet *objcEnvironmentVariables = [environment keysOfEntriesPassingTest:^(NSString *variable, id obj, BOOL *stop) {
return [variable hasPrefix:@"OBJC"];
}];
[environment removeObjectsForKeys:objcEnvironmentVariables.allObjects];
return environment;
};
NSTask * (^codesignTaskWithArguments)(NSArray *) = ^ (NSArray *arguments) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/xcrun";
task.arguments = [@[ @"codesign" ] arrayByAddingObjectsFromArray:arguments];
task.environment = environmentSuitableForChildProcess();
return task;
};
void (^resignTestApplicationPreserveEverythingButTheRequirements)(void) = ^{
NSURL *testApplicationLocation = self.testApplicationURL;
NSTask *resignCodesignTask = codesignTaskWithArguments(@[
@"--sign", @"-",
@"--force",
@"--verbose=4",
@"--preserve-metadata=identifier,entitlements,resource-rules",
testApplicationLocation.path,
]);
[resignCodesignTask launch];
[resignCodesignTask waitUntilExit];
expect(resignCodesignTask.terminationStatus).to.equal(0);
};
void (^deepCodesignTestApplication)(void) = ^{
NSURL *testApplicationLocation = self.testApplicationURL;
NSBundle *testsBundle = [NSBundle bundleForClass:self.class];
NSURL *deepCodesignLocation = [testsBundle URLForResource:@"deep-codesign" withExtension:nil];
NSNumber *executable = nil;
NSError *executableError = nil;
BOOL getExecutable = [deepCodesignLocation getResourceValue:&executable forKey:NSURLIsExecutableKey error:&executableError];
expect(getExecutable).to.beTruthy();
expect(executable.boolValue).to.beTruthy();
expect(executableError).to.beNil();
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];
[deepCodesignTask waitUntilExit];
expect(deepCodesignTask.terminationStatus).to.equal(0);
/*
By signing test application's contents, which are covered by test
application's ResourceRules it's signature becomes invalid. Usually,
Xcode would sign the application after deep-codesign has run so the
signature would include the _signed_ child resources which are covered
by test application's ResourceRules
test application is already signed, so we need to manually resign it
deep-codesign preserves the requirements, we can't use it to resign the
root target, the new signing identity wouldn't verify against the
original designated requirement
*/
resignTestApplicationPreserveEverythingButTheRequirements();
};
BOOL (^deepVerify)(void) = ^ BOOL {
NSTask *deepVerifyTask = codesignTaskWithArguments(@[
@"--deep-verify",
@"--verbose=4",
self.testApplicationURL.path
]);
[deepVerifyTask launch];
[deepVerifyTask waitUntilExit];
return deepVerifyTask.terminationStatus == 0;
};
it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
xit(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
});
SpecEnd
+14 -19
View File
@@ -6,44 +6,39 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
QuickSpecBegin(SQRLDirectoryManagerSpec)
SpecBegin(SQRLDirectoryManager)
__block NSString *otestIdentifier;
beforeEach(^{
otestIdentifier = NSProcessInfo.processInfo.environment[@"FORCE_APP_IDENTIFIER"];
expect(otestIdentifier).notTo(beNil());
otestIdentifier = NSRunningApplication.currentApplication.localizedName;
expect(otestIdentifier).notTo.beNil();
});
it(@"should initialize", ^{
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier];
expect(manager).notTo(beNil());
expect(manager).notTo.beNil();
});
it(@"should create a manager for the current app", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
expect(manager).notTo(beNil());
expect(manager).to(equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]));
expect(manager).notTo.beNil();
expect(manager).to.equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]);
});
it(@"should send an Application Support URL", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
NSError *error = nil;
NSURL *appSupportURL = [[manager storageURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo(beNil());
expect(error).to(beNil());
NSURL *appSupportURL = [[manager applicationSupportURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo.beNil();
expect(error).to.beNil();
__block BOOL directory = NO;
expect(@([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory])).to(beTruthy());
expect(@(directory)).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory]).to.beTruthy();
expect(directory).to.beTruthy();
});
it(@"should send a ShipIt state URL", ^{
@@ -51,8 +46,8 @@ it(@"should send a ShipIt state URL", ^{
NSError *error = nil;
NSURL *stateURL = [[manager shipItStateURL] firstOrDefault:nil success:NULL error:&error];
expect(stateURL).notTo(beNil());
expect(error).to(beNil());
expect(stateURL).notTo.beNil();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+91 -128
View File
@@ -6,32 +6,13 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller+Private.h"
#import "SQRLInstallerOwnedBundle.h"
#import "SQRLShipItRequest.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLInstallerSpec)
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);
};
SpecBegin(SQRLInstaller)
__block NSURL *updateURL;
@@ -39,148 +20,127 @@ beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
it(@"should install an update using ShipIt", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName: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];
[self launchShipIt];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should install an update in process", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
[self installWithRequest:request remote:NO];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should install an update and relaunch", ^{
NSString *bundleIdentifier = @"com.github.Squirrel.TestApplication";
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier];
expect(@(apps.count)).to(equal(@0));
expect(apps.count).to.equal(0);
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO];
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).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count)).toEventually(equal(@1));
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count).will.equal(1);
});
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 useUpdateBundleName: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).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
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 useUpdateBundleName: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]).withTimeout(SQRLLongTimeout).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
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;
beforeEach(^{
targetURL = self.testApplicationURL;
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
expect(installer).notTo.beNil();
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());
});
it(@"should not install an update after too many attempts", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
[self installWithRequest:request remote:YES];
__block NSError *error;
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).toEventually(beTruthy());
expect(error).to(beNil());
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
it(@"should relaunch even after failing to install an update", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO];
[self installWithRequest:request remote:YES];
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count)).toEventually(equal(@1));
__block NSError *error;
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).to(beTruthy());
expect(error).to(beNil());
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
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 useUpdateBundleName: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).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
expect(@(modeOfURL(self.testApplicationURL))).to(equal(@0755));
expect(@(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0755));
it(@"should relaunch even after failing to install an update", ^{
NSURL *targetURL = self.testApplicationURL;
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.backupBundleURL = backupURL;
state.installerState = SQRLInstallerStateInstalling;
state.installationStateAttempt = 4;
state.relaunchAfterInstallation = YES;
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self launchShipIt];
__block NSError *error = nil;
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
expect(error).to.beNil();
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
describe(@"signal handling", ^{
__block NSURL *targetURL;
void (^verifyUpdate)(void) = ^{
// Wait up to the launchd throttle interval, then verify that ShipIt
// relaunched and finished installing the update.
expect(self.testApplicationBundleVersion).withTimeout(SQRLLongTimeout).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
NSError *error;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
};
beforeEach(^{
// Copied so that we don't recreate the TestApplication bundle by
// accessing the property.
targetURL = self.testApplicationURL;
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self installWithRequest:request remote:YES];
[self launchShipIt];
// Wait until ShipIt has transitioned by at least one state.
expect([[[SQRLShipItState readUsingURL:self.shipItDirectoryManager.shipItStateURL] asynchronousFirstOrDefault:nil success:NULL error:NULL] installerState]).willNot.equal(SQRLInstallerStateNothingToDo);
// Apply a random delay before sending the termination signal, to
// fuzz out race conditions.
@@ -188,42 +148,45 @@ describe(@"signal handling", ^{
[NSThread sleepForTimeInterval:delay];
});
afterEach(^{
// Wait up to the launchd throttle interval, then verify that ShipIt
// relaunched and finished installing the update.
Expecta.asynchronousTestTimeout = 5;
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
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");
verifyUpdate();
});
it(@"should handle SIGTERM", ^{
system("killall -TERM ShipIt");
verifyUpdate();
});
it(@"should handle SIGINT", ^{
system("killall -INT ShipIt");
verifyUpdate();
});
it(@"should handle SIGQUIT", ^{
system("killall -QUIT ShipIt");
verifyUpdate();
});
it(@"should handle SIGKILL", ^{
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
// a couple times to really test resumption.
//
// Two SIGKILL signals means that it'll be launched three times (which
// matches the maximum number of attempts per state).
for (int i = 0; i < 2; i++) {
// a few times to really test resumption.
for (int i = 0; i < 3; i++) {
system("killall -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
[NSThread sleepForTimeInterval:delay];
}
verifyUpdate();
});
});
QuickSpecEnd
SpecEnd
-86
View File
@@ -1,86 +0,0 @@
//
// SQRLShipItStateSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-09.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLShipItRequest.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLShipItRequestSpec)
__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 useUpdateBundleName:NO];
expect(request).notTo(beNil());
expect(request.targetBundleURL).to(equal(self.testApplicationURL));
expect(request.updateBundleURL).to(equal(updateURL));
expect(request.bundleIdentifier).to(beNil());
expect(@(request.launchAfterInstallation)).to(beFalsy());
});
afterEach(^{
NSURL *stateURL = [[directoryManager shipItStateURL] first];
expect(stateURL).notTo(beNil());
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
});
it(@"should copy", ^{
SQRLShipItRequest *requestCopy = [request copy];
expect(requestCopy).to(equal(request));
expect(requestCopy).notTo(beIdenticalTo(request));
});
it(@"should fail to read state when no file exists yet", ^{
NSError *error;
BOOL success = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(error).notTo(beNil());
});
it(@"should write and read state to disk", ^{
NSError *error;
BOOL success = [[request writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(readRequest).to(equal(request));
});
it(@"should fail gracefully with archives encoding a different class", ^{
NSURL *archiveLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"archive"];
NSError *error;
BOOL write = [[NSKeyedArchiver archivedDataWithRootObject:@"rogue object"] writeToURL:archiveLocation atomically:YES];
expect(@(write)).to(beTruthy());
expect(error).to(beNil());
BOOL success = NO;
SQRLShipItRequest *request = [[SQRLShipItRequest readUsingURL:[RACSignal return:archiveLocation]] firstOrDefault:nil success:&success error:&error];
expect(request).to(beNil());
expect(@(success)).to(beFalsy());
expect(error.domain).to(equal(SQRLShipItRequestErrorDomain));
expect(@(error.code)).to(equal(@(SQRLShipItRequestErrorUnarchiving)));
});
QuickSpecEnd
+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
+13 -24
View File
@@ -6,22 +6,15 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLTerminationListener.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLTerminationListenerSpec)
SpecBegin(SQRLTerminationListener)
__block SQRLTerminationListener *listener;
beforeEach(^{
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
expect(listener).notTo(beNil());
expect(listener).notTo.beNil();
});
it(@"should complete immediately when the app is not running", ^{
@@ -30,12 +23,10 @@ it(@"should complete immediately when the app is not running", ^{
completed = YES;
}];
expect(@(completed)).to(beTruthy());
expect(completed).to.beTruthy();
});
it(@"should wait until one instance terminates", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSRunningApplication *app = [self launchTestApplicationWithEnvironment:nil];
__block NSRunningApplication *observedApp = nil;
@@ -46,17 +37,15 @@ it(@"should wait until one instance terminates", ^{
completed = YES;
}];
expect(observedApp).toEventually(equal(app));
expect(@(completed)).to(beFalsy());
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
[app forceTerminate];
expect(@(app.terminated)).toEventually(beTruthy());
expect(@(completed)).toEventually(beTruthy());
expect(app.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
it(@"should wait until multiple instances terminate", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSRunningApplication *app1 = [self launchTestApplicationWithEnvironment:nil];
NSRunningApplication *app2 = [self launchTestApplicationWithEnvironment:nil];
@@ -65,15 +54,15 @@ it(@"should wait until multiple instances terminate", ^{
completed = YES;
}];
expect(@(completed)).to(beFalsy());
expect(completed).to.beFalsy();
[app1 forceTerminate];
expect(@(app1.terminated)).toEventually(beTruthy());
expect(@(completed)).to(beFalsy());
expect(app1.terminated).will.beTruthy();
expect(completed).to.beFalsy();
[app2 forceTerminate];
expect(@(app2.terminated)).toEventually(beTruthy());
expect(@(completed)).toEventually(beTruthy());
expect(app2.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
QuickSpecEnd
SpecEnd
@@ -1,19 +1,11 @@
//
// QuickSpec+SQRLFixtures.h
// SQRLTestCase.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
/// See https://github.com/Squirrel/Squirrel.Mac/pull/154#issuecomment-151287744
#define SKIP_IF_RUNNING_ON_TRAVIS if (self.runningOnTravis) return;
// The short version string for the `testApplicationBundle`.
extern NSString * const SQRLTestApplicationOriginalShortVersionString;
@@ -24,15 +16,10 @@ extern NSString * const SQRLTestApplicationUpdatedShortVersionString;
// The Info.plist key associated with a bundle's short version string.
extern NSString * const SQRLBundleShortVersionStringKey;
/// The longer timeout we should use for things that take a long time. This is
/// especially needed on slower machines (e.g., Travis CI).
extern const NSTimeInterval SQRLLongTimeout;
@class SQRLCodeSignature;
@class SQRLDirectoryManager;
@class SQRLShipItRequest;
@interface QuickSpec (SQRLFixtures)
@interface SQRLTestCase : SPTSenTestCase
// A URL to a temporary directory tests can use.
//
@@ -58,12 +45,13 @@ extern const NSTimeInterval SQRLLongTimeout;
// 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;
/// Are the tests currently being run on Travis?
@property (nonatomic, readonly, assign, getter = isRunningOnTravis) BOOL runningOnTravis;
// Launches a new copy of TestApplication.app from the `testApplicationBundle`.
//
// Invoking this method multiple times will result in multiple running instances
@@ -86,8 +74,8 @@ extern const NSTimeInterval SQRLLongTimeout;
// deleted at the end of the example.
- (NSURL *)createTestApplicationUpdate;
// Runs the installer in process or submits ShipIt's launchd job to start it up.
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
// Submits ShipIt's launchd job to start it up.
- (void)launchShipIt;
// Creates a disk image, then mounts it.
//
@@ -1,20 +1,16 @@
//
// QuickSpec+SQRLFixtures.m
// SQRLTestCase.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLTestCase.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItRequest.h"
#import <ServiceManagement/ServiceManagement.h>
#import <objc/runtime.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
@@ -23,8 +19,6 @@ NSString * const SQRLTestApplicationOriginalShortVersionString = @"1.0";
NSString * const SQRLTestApplicationUpdatedShortVersionString = @"2.1";
NSString * const SQRLBundleShortVersionStringKey = @"CFBundleShortVersionString";
const NSTimeInterval SQRLLongTimeout = 20;
static void SQRLKillAllTestApplications(void) {
// Forcibly kill all copies of the TestApplication that may be running.
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"];
@@ -45,25 +39,40 @@ static void SQRLSignalHandler(int sig) {
abort();
}
static NSBundle *SQRLTestBundle(void) {
return [NSBundle bundleWithIdentifier:@"com.github.SquirrelTests"];
}
@interface SQRLTestCase ()
static NSMutableArray *cleanupBlocks = nil;
// An array of dispatch_block_t values, for performing cleanup after the current
// example finishes.
//
// This array will be emptied between each example.
@property (nonatomic, strong, readonly) NSMutableArray *exampleCleanupBlocks;
// The URL to the temporary directory which contains `temporaryDirectoryURL` and
// all copied test data.
static NSURL *baseTemporaryDirectoryURL = nil;
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
QuickConfigurationBegin(Fixtures)
// Returns an _unlaunched_ task that will follow log files at the given paths,
// then pipe that output through this process.
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths;
+ (void)configure:(Configuration *)configuration {
@end
@implementation SQRLTestCase
#pragma mark Properties
@synthesize baseTemporaryDirectoryURL = _baseTemporaryDirectoryURL;
#pragma mark Lifecycle
+ (void)load {
NSURL *appSupportURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
NSAssert(appSupportURL != nil, @"Could not find Application Support folder");
NSArray *folders = @[
@"com.github.Squirrel.TestApplication.ShipIt",
@"com.github.Squirrel.SquirrelTests.ShipIt",
@"otest.ShipIt",
@"otest-x86_64.ShipIt",
];
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
@@ -74,69 +83,93 @@ QuickConfigurationBegin(Fixtures)
].rac_sequence;
}];
[configuration beforeSuite:^{
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
for (NSURL *URL in URLs) {
[[NSData data] writeToURL:URL atomically:YES];
}
cleanupBlocks = [NSMutableArray array];
for (NSURL *URL in URLs) {
[NSFileManager.defaultManager createDirectoryAtURL:URL.URLByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:NULL];
if (![[NSData data] writeToURL:URL atomically:YES]) {
NSLog(@"Could not touch log file at %@", URL);
}
}
RACSequence *paths = [URLs map:^(NSURL *URL) {
return URL.path;
}];
// We want to run any enqueued cleanup blocks after _and before_ each spec,
// in case beforeSuites (for example) use the fixtures.
void (^runCleanupBlocks)(void) = ^{
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
block();
}
NSTask *readShipIt = [self tailTaskWithPaths:paths];
[readShipIt launch];
[cleanupBlocks removeAllObjects];
};
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
[configuration beforeEach:^{
runCleanupBlocks();
}];
[configuration afterEach:^{
runCleanupBlocks();
SQRLKillAllTestApplications();
}];
atexit_b(^{
[readShipIt terminate];
});
}
QuickConfigurationEnd
- (void)setUp {
[super setUp];
@implementation QuickSpec (SQRLFixtures)
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
#pragma mark Lifecycle
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];
SQRLKillAllTestApplications();
}
- (void)addCleanupBlock:(dispatch_block_t)block {
[cleanupBlocks addObject:[block copy]];
[self.exampleCleanupBlocks addObject:[block copy]];
}
#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;
}
#pragma mark Temporary Directory
- (NSURL *)baseTemporaryDirectoryURL {
if (baseTemporaryDirectoryURL == nil) {
if (_baseTemporaryDirectoryURL == nil) {
NSURL *globalTemporaryDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
_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);
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:_baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
[self addCleanupBlock:^{
[NSFileManager.defaultManager removeItemAtURL:baseTemporaryDirectoryURL error:NULL];
baseTemporaryDirectoryURL = nil;
[NSFileManager.defaultManager removeItemAtURL:_baseTemporaryDirectoryURL error:NULL];
_baseTemporaryDirectoryURL = nil;
}];
}
return baseTemporaryDirectoryURL;
return _baseTemporaryDirectoryURL;
}
- (NSURL *)temporaryDirectoryURL {
@@ -144,7 +177,7 @@ QuickConfigurationEnd
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;
}
@@ -154,15 +187,24 @@ QuickConfigurationEnd
- (NSURL *)testApplicationURL {
NSURL *fixtureURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app" isDirectory:YES];
if (![NSFileManager.defaultManager fileExistsAtPath:fixtureURL.path]) {
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
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];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
[readTestApp terminate];
}];
}
return fixtureURL;
@@ -171,8 +213,8 @@ QuickConfigurationEnd
- (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;
}
@@ -191,7 +233,7 @@ QuickConfigurationEnd
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) {
@@ -211,12 +253,12 @@ QuickConfigurationEnd
}
- (NSURL *)createTestApplicationUpdate {
NSURL *originalURL = [SQRLTestBundle() URLForResource:@"TestApplication 2.1" withExtension:@"app"];
XCTAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
NSURL *originalURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication 2.1" withExtension:@"app"];
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];
@@ -224,18 +266,18 @@ QuickConfigurationEnd
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 = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
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);
@@ -243,7 +285,7 @@ QuickConfigurationEnd
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);
@@ -253,57 +295,48 @@ QuickConfigurationEnd
}
- (SQRLCodeSignature *)testApplicationSignature {
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
return [[SQRLCodeSignature alloc] initWithRequirement:requirement];
}];
}
NSError *error = nil;
SQRLCodeSignature *signature = [SQRLCodeSignature signatureWithBundle:bundleURL error:&error];
XCTAssertNotNil(signature, @"Error getting signature for bundle at %@: %@", bundleURL, error);
- (NSData *)testApplicationCodeSigningRequirementData {
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
CFDataRef data = NULL;
OSStatus status = SecRequirementCopyData(requirement, kSecCSDefaultFlags, &data);
STAssertTrue(status == noErr, @"Error copying data for requirement %@", requirement);
return signature;
return CFBridgingRelease(data);
}];
}
- (SQRLDirectoryManager *)shipItDirectoryManager {
NSString *identifier = SQRLShipItLauncher.shipItJobLabel;
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:identifier];
XCTAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
STAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
return manager;
}
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote {
if (remote) {
expect(@([[request writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL])).to(beTruthy());
- (void)launchShipIt {
NSError *error = nil;
STAssertTrue([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error], @"Could not launch ShipIt: %@", error);
__block NSError *error = nil;
expect(@([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error])).to(beTruthy());
expect(error).to(beNil());
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
NSError *lookupError;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
expect(stateURL).notTo(beNil());
expect(lookupError).to(beNil());
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
}];
} else {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:self.shipItDirectoryManager.applicationIdentifier];
expect(installer).notTo(beNil());
NSError *installedError = nil;
BOOL installed = [[installer.installUpdateCommand execute:request] asynchronouslyWaitUntilCompleted:&installedError];
expect(@(installed)).to(beTruthy());
expect(installedError).to(beNil());
}
NSError *lookupError = nil;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
STAssertNotNil(stateURL, @"Could not find state URL from %@: %@", self.shipItDirectoryManager, lookupError);
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
}];
}
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL {
NSURL *destinationURL = [self.baseTemporaryDirectoryURL URLByAppendingPathComponent:name];
XCTAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
STAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
NSString *createInvocation;
if (directoryURL == nil) {
@@ -312,24 +345,20 @@ QuickConfigurationEnd
createInvocation = [NSString stringWithFormat:@"hdiutil create '%@' -fs 'HFS+' -volname '%@' -format UDSP -size 10m -srcfolder '%@' -quiet", destinationURL.path, name, directoryURL.path];
}
expect(@(system(createInvocation.UTF8String))).to(equal(@0));
expect(system(createInvocation.UTF8String)).to.equal(0);
NSString *mountInvocation = [NSString stringWithFormat:@"hdiutil attach '%@.sparseimage' -noverify -noautofsck -readwrite -quiet", destinationURL.path];
expect(@(system(mountInvocation.UTF8String))).to(equal(@0));
expect(system(mountInvocation.UTF8String)).to.equal(0);
NSString *path = [NSString stringWithFormat:@"/Volumes/%@", name];
[self addCleanupBlock:^{
NSString *detachInvocation = [NSString stringWithFormat:@"hdiutil detach '%@' -force -quiet", path];
expect(@(system(detachInvocation.UTF8String))).to(equal(@0));
expect(system(detachInvocation.UTF8String)).to.equal(0);
}];
return [NSURL fileURLWithPath:path isDirectory:YES];
}
- (BOOL)isRunningOnTravis {
return NSProcessInfo.processInfo.environment[@"TRAVIS"] != nil;
}
@end
#pragma clang diagnostic pop
+14 -19
View File
@@ -6,27 +6,22 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(SQRLUpdateSpec)
SpecBegin(SQRLUpdate)
it(@"should return nil when initialised without a url", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{} error:&error];
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
});
it(@"should return nil when initialised with a url not in URL syntax", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{ @"updateURL": [NSURL URLWithString:@"test"] } error:&error];
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
});
it(@"should validate release name and notes", ^{
@@ -37,20 +32,20 @@ it(@"should validate release name and notes", ^{
@"releaseNotes": [[NSObject alloc] init]
} error:NULL];
expect(update).notTo(beNil());
expect(update.updateURL).to(equal(updateURL));
expect(update.releaseName).to(beNil());
expect(update.releaseNotes).to(beNil());
expect(update).notTo.beNil();
expect(update.updateURL).to.equal(updateURL);
expect(update.releaseName).to.beNil();
expect(update.releaseNotes).to.beNil();
});
it(@"should parse Central style dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:NULL];
expect(update.releaseDate).to(equal([NSDate dateWithTimeIntervalSince1970:1379438667]));
expect(update.releaseDate).notTo.beNil();
});
it(@"should parse ISO 8601 dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:NULL];
expect(update.releaseDate).to(equal([NSDate dateWithTimeIntervalSince1970:1379506627]));
expect(update.releaseDate).notTo.beNil();
});
QuickSpecEnd
SpecEnd
+47 -119
View File
@@ -6,44 +6,37 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLTestUpdate.h"
#import "TestAppConstants.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
QuickSpecBegin(SQRLUpdaterSpec)
SpecBegin(SQRLUpdater)
__block NSURL *JSONURL;
void (^writeUpdate)(SQRLUpdate *) = ^(SQRLUpdate *update) {
NSDictionary *updateInfo = [MTLJSONAdapter JSONDictionaryFromModel:update];
expect(updateInfo).notTo(beNil());
expect(updateInfo).notTo.beNil();
NSError *error = nil;
NSData *JSON = [NSJSONSerialization dataWithJSONObject:updateInfo options:NSJSONWritingPrettyPrinted error:&error];
expect(JSON).notTo(beNil());
expect(error).to(beNil());
expect(JSON).notTo.beNil();
expect(error).to.beNil();
BOOL success = [JSON writeToURL:JSONURL options:NSDataWritingAtomic error:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
};
NSURL * (^zipUpdate)(NSURL *) = ^(NSURL *updateURL) {
NSURL *zipFolderURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:self.temporaryDirectoryURL create:YES error:NULL];
expect(zipFolderURL).notTo(beNil());
expect(zipFolderURL).notTo.beNil();
NSURL *zippedUpdateURL = [[zipFolderURL URLByAppendingPathComponent:updateURL.lastPathComponent] URLByAppendingPathExtension:@"zip"];
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zippedUpdateURL fromDirectoryAtURL:updateURL] asynchronouslyWaitUntilCompleted:NULL];
expect(@(success)).to(beTruthy());
expect(success).to.beTruthy();
return zippedUpdateURL;
};
@@ -61,34 +54,30 @@ beforeEach(^{
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should not install a corrupt update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@@ -98,16 +87,14 @@ describe(@"updating", ^{
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(app.terminated).will.beTruthy();
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should update to the most recently enqueued job", ^{
SKIP_IF_RUNNING_ON_TRAVIS
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
@@ -131,13 +118,11 @@ describe(@"updating", ^{
writeUpdate(update);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
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", ^{
SKIP_IF_RUNNING_ON_TRAVIS
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
@@ -145,22 +130,24 @@ describe(@"updating", ^{
writeUpdate(update);
NSTimeInterval delay = 15;
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
expect(@(app.terminated)).withTimeout(delay + SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
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 storageURL] first];
expect(appSupportURL).notTo(beNil());
appSupportURL = [[directoryManager applicationSupportURL] first];
expect(appSupportURL).notTo.beNil();
updateDirectoryURLs = [[RACSignal
defer:^{
@@ -175,25 +162,28 @@ describe(@"updating", ^{
});
it(@"should remove downloaded archives after updating", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateRequestCount": @2 });
expect([updateDirectoryURLs toArray]).toEventuallyNot(equal(@[]));
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([updateDirectoryURLs toArray]).toEventually(equal(@[]));
// Also test that this is the only update directory (any others
// should've been removed).
NSArray *directoryURLs = [updateDirectoryURLs toArray];
expect(directoryURLs.count).to.equal(1);
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
expect(contents).to.equal(@[]);
});
});
});
@@ -202,7 +192,7 @@ describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
beforeEach(^{
before(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
@@ -219,9 +209,9 @@ describe(@"response handling", ^{
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)));
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
});
it(@"should return an error for non JSON data", ^{
@@ -236,72 +226,10 @@ describe(@"response handling", ^{
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)));
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
});
});
static RACSignal * (^stateNotificationListener)(void) = ^ {
return [[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil]
map:^(NSNotification *notification) {
return notification.userInfo[SQRLTestAppUpdaterStateKey];
}]
setNameWithFormat:@"stateNotificationListener"];
};
describe(@"state", ^{
it(@"should transition through idle, checking and idle, when there is no update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
}];
NSRunningApplication *testApplication = launchWithEnvironment(nil);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateIdle),
];
expect(states).toEventually(equal(expectedStates));
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
});
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
}];
NSError *error;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate([self createTestApplicationUpdate]),
@"final": @YES,
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
writeUpdate(update);
NSRunningApplication *testApplication = launchWithEnvironment(nil);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateDownloadingUpdate),
@(SQRLUpdaterStateAwaitingRelaunch),
];
expect(states).withTimeout(SQRLLongTimeout).toEventually(equal(expectedStates));
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
});
});
QuickSpecEnd
SpecEnd
+20 -26
View File
@@ -6,44 +6,38 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
QuickSpecBegin(SQRLZipArchiverSpec)
SpecBegin(SQRLZipArchiver)
it(@"should extract a zip archive created by the Finder", ^{
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication 2.1.app"];
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should fail to extract a nonexistent zip archive", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:[self.temporaryDirectoryURL URLByAppendingPathComponent:@"foo.zip"] intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
NSLog(@"%@", error);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLZipArchiverErrorDomain));
expect(@(error.code)).to(equal(@(SQRLZipArchiverShellTaskFailed)));
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo(equal(0));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
});
it(@"should create a zip archive readable by itself", ^{
@@ -51,21 +45,21 @@ it(@"should create a zip archive readable by itself", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zipURL fromDirectoryAtURL:self.testApplicationURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@([NSFileManager.defaultManager fileExistsAtPath:zipURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:zipURL.path]).to.beTruthy();
success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app"];
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+1 -1
View File
@@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
+25
View File
@@ -0,0 +1,25 @@
//
// SquirrelTests-Prefix.pch
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-07-21.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SenTestingKit/SenTestingKit.h>
#import <Mantle/Mantle.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import <Squirrel/Squirrel.h>
#define SPT_SUBCLASS SQRLTestCase
#import "Specta.h"
#define EXP_SHORTHAND
#import "Expecta.h"
#import "SQRLTestCase.h"
#endif
-19
View File
@@ -1,19 +0,0 @@
//
// SwiftSpec.swift
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2014-10-14.
// Copyright (c) 2014 GitHub. All rights reserved.
//
import Foundation
import Nimble
import Quick
// Without this, the Swift stdlib won't be linked into the test target (even if
// “Embedded Content Contains Swift Code” is enabled).
class SwiftSpec: QuickSpec {
override func spec() {
expect(true).to(beTruthy())
}
}
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>rules</key>
<dict>
<key>.*</key>
<true/>
</dict>
</dict>
</plist>
-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";
+1 -11
View File
@@ -12,7 +12,6 @@
#import "SQRLTestUpdate.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "TestAppConstants.h"
@interface TestAppDelegate ()
@@ -64,17 +63,8 @@
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;
NSInteger updateRequestCount = [NSProcessInfo.processInfo.environment[@"SQRLUpdateRequestCount"] integerValue];
if (updateRequestCount < 1) updateRequestCount = 1;
[[[[[[[[[[RACSignal
defer:^{
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
@@ -93,7 +83,7 @@
return testUpdate.final;
}]
take:updateRequestCount]
take:1]
doNext:^(id _) {
NSLog(@"***** READY TO INSTALL UPDATE *****");
}]
+1 -3
View File
@@ -9,7 +9,7 @@
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>com.github.Squirrel.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
@@ -30,7 +30,5 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<false/>
</dict>
</plist>
+1 -1
View File
@@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>${PRODUCT_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
+2 -9
View File
@@ -10,7 +10,7 @@ config ()
{
# A whitespace-separated list of executables that must be present and locatable.
: ${REQUIRED_TOOLS="xctool"}
export REQUIRED_TOOLS
}
@@ -28,14 +28,7 @@ main ()
check_deps
fi
local submodules=$(git submodule status)
local result=$?
if [ "$result" -ne "0" ]
then
exit $result
fi
local submodules=$(git submodule status 2>/dev/null)
if [ -n "$submodules" ]
then
echo "*** Updating submodules..."
BIN
View File
Binary file not shown.
+19 -26
View File
@@ -41,26 +41,30 @@ config ()
# 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
}
##
## Code Signing
##
setup_keychain ()
unlock_keychain ()
{
KEY_CHAIN=build.keychain
security create-keychain -p travis $KEY_CHAIN
security default-keychain -s $KEY_CHAIN
security unlock-keychain -p travis $KEY_CHAIN
security set-keychain-settings -t 3600 -u $KEY_CHAIN
security import script/certs.p12 -k build.keychain -P $KEY_PASSWORD -T /usr/bin/codesign
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
}
##
@@ -70,7 +74,7 @@ setup_keychain ()
main ()
{
config
setup_keychain
unlock_keychain
if [ -f "$BOOTSTRAP" ]
then
@@ -136,29 +140,18 @@ build_scheme ()
local awkstatus=$?
if [ "$awkstatus" -eq "1" ]
then
# SDK not found, try for iphonesimulator.
sdkflag="-sdk iphonesimulator"
# Determine whether the unit tests will run with iphonesimulator
run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build
awkstatus=$?
if [ "$awkstatus" -ne "0" ]
then
# Unit tests will not run on iphonesimulator.
sdkflag=""
fi
fi
if [ "$awkstatus" -ne "0" ]
then
# Unit tests aren't supported.
action=build
fi
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env ruby
def exit_error(description)
puts "{ \"error\": \"#{description}\" }"
exit 1
end
module Targets
class Object
def self.match(path)
!File.symlink?(path)
end
attr_reader :path
def initialize(path)
@path = path
end
def search_directories
[]
end
end
class Bundle < Object
def self.match(path)
return false unless super
return false unless match = File.extname(path).match(/\.(.+)/)
return match[1] == extension
end
def contents_directories
[ "Contents" ]
end
def suffix_directories
[
"Frameworks",
"SharedFrameworks",
"XPCServices",
"PlugIns",
"Support",
"SharedSupport",
File.join("Library", "LoginItems"),
File.join("Library", "LaunchServices"),
]
end
def search_directories
contents_directories.product(suffix_directories).map do |combination|
File.join(*combination)
end
end
end
class VersionedBundle < Bundle
def contents_directories
contents_directories = []
versions_directory = File.join(@path, "Versions")
Dir.foreach(versions_directory) do |current_version|
next if current_version.start_with?('.')
full_path = File.join(versions_directory, current_version)
next if File.symlink?(full_path)
contents_directories << File.join("Versions", current_version)
end
contents_directories
end
end
class App < Bundle
def self.extension
"app"
end
end
class Framework < VersionedBundle
def self.extension
"framework"
end
end
class XPCService < Bundle
def self.extension
"xpc"
end
end
class Tool < Object
def self.match(path)
return false unless super
return false unless File.executable?(path)
# Look for an __TEXT,__info_plist section
# This contains the CFBundleIdentifier necessary for signing
`otool -l #{path}` =~ /__info_plist/
end
end
def self.all_targets
[ App, Framework, XPCService, Tool ]
end
end
module Codesign
class Task
def self.sign(target, identity)
codesign_with_arguments([ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,resource-rules,requirements", "--verbose", target.path ])
end
protected
def self.codesign_with_arguments(arguments)
$stdout.flush
pid = fork
if pid.nil?
exec("/usr/bin/xcrun", *([ "codesign" ].concat(arguments)))
else
Process.wait(pid)
exit_error("codesign failed") unless $?.exitstatus == 0
end
end
end
end
codesign_identity = ENV['CODE_SIGN_IDENTITY']
unless codesign_identity
exit_error("No codesign identity provided")
end
build_dir = ENV['CONFIGURATION_BUILD_DIR']
product_name = ENV['FULL_PRODUCT_NAME']
unless build_dir && product_name
exit_error("Path to built product not provided")
end
input_path = File.join(build_dir, product_name)
unless File.exists?(input_path)
exit_error("Built product doesn't exist at path provided")
end
targets = [ ]
def match_path(path)
Targets.all_targets.each do |target|
return target.new(path) if target.match(path)
end
nil
end
root_target = match_path input_path
if root_target.nil?
exit_error("Built product is not a known file structure, cannot deep codesign")
end
# Recursion base case can't use build_dir because there can be >1 product
paths_to_search = root_target.search_directories.map do |current_dir|
File.join(root_target.path, current_dir)
end
while paths_to_search.size > 0
new_search_paths = []
paths_to_search.each do |search_path|
next unless File.directory?(search_path)
Dir.foreach(search_path) do |current_directory_entry|
next if current_directory_entry.start_with?(".")
target = match_path(File.join(search_path, current_directory_entry))
next if target.nil?
targets << target
new_search_paths << target.search_directories.map do |current_search_directory|
File.join(target.path, current_search_directory)
end
end
end
paths_to_search = new_search_paths.flatten
end
puts "Signing these targets:"
puts "="*22
print "\n"
# Sort into nested-most first, outer targets might include inner targets
# in their ResourceRules
targets = targets.sort_by do |target|
target.path.size
end
targets.reverse!
targets.each do |target|
puts "=> #{target.path}"
Codesign::Task.sign(target, codesign_identity)
end
+3 -1
View File
@@ -2,8 +2,10 @@ BEGIN {
FS = "\n";
}
/Schemes:/ {
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}