Author SHA1 Message Date
Justin Spahr-Summers 33137bcae6 Fix update directories not being pruned (bad merge) 2014-01-24 23:09:33 -08:00
Justin Spahr-Summers 532c565b2a Need to -flattenMap: to the list of directories 2014-01-24 23:07:24 -08:00
Justin Spahr-Summers 9f5a0761ae Ignore the RACUnit we use to hack lazy memoization 2014-01-24 23:03:48 -08:00
Justin Spahr-Summers 572ef9aa9b Update RACAction usage, remove RACPromises 2014-01-24 23:01:00 -08:00
Justin Spahr-Summers b8aef91738 Merge branch 'master' into rac-3.0
Conflicts:
	External/ReactiveCocoa
	Squirrel/SQRLShipItLauncher.m
	Squirrel/SQRLUpdater.m
2014-01-24 22:48:31 -08:00
Josh Abernathy 36e0531218 Merge pull request #104 from Squirrel/dont-crash-with-no-signature
Log when we can’t get the running application code signature
2014-01-22 11:18:39 -08:00
Keith Duncan 77aec66456 Throw an exception when there’s no code signature in release 2014-01-22 11:08:31 -08:00
Keith Duncan e8523fd74d Log when we can’t get the running application code signature
Don’t crash.
2014-01-22 10:37:58 -08:00
Justin Spahr-Summers 767892c788 ++RAC to fix RACCommand KVO crashes 2014-01-21 14:36:05 -08:00
Rob Rix 34e37569d7 Merge pull request #103 from Squirrel/prune-old-updates
Prune temporary directories before performing first update check
2014-01-21 14:25:55 -08:00
Justin Spahr-Summers 4c93cb4508 Remove update directories before first check of any kind
Not just before beginning automatic updates.
2014-01-21 12:26:43 -08:00
Justin Spahr-Summers 431224c788 Test that updates are cleaned up 2014-01-21 12:16:35 -08:00
Justin Spahr-Summers ea8f805825 Remove old update directories before checking for updates 2014-01-21 11:55:52 -08:00
Justin Spahr-Summers 7bb4c131e1 Remove downloaded zips after unarchiving them 2014-01-21 11:43:32 -08:00
Justin Spahr-Summers 372a7d783a Merge pull request #101 from Squirrel/update-readme
Add steps to Readme
2014-01-20 15:57:27 -08:00
Justin Spahr-Summers b85e03fbe9 Minor ordering/phrasing tweaks 2014-01-20 15:56:31 -08:00
probablycorey 48455f1ee4 Add steps to get Squirrel to build 2014-01-20 15:47:46 -08:00
Justin Spahr-Summers 5cd5fdb2ed Merge pull request #97 from Squirrel/bridge-authorization
Don’t `__bridge` cast the `AuthorizationRef`
2014-01-07 10:49:48 -08:00
Keith Duncan aa545ed419 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	External/ReactiveCocoa
2014-01-07 18:06:35 +00:00
Keith Duncan 3d507a58a6 Check that _authorization is non NULL before freeing 2014-01-07 17:25:54 +00:00
Justin Spahr-Summers 406728c066 ++RAC 2014-01-06 12:09:29 -08:00
Keith Duncan 53adc17554 Types++ 2014-01-06 12:05:58 +00:00
Keith Duncan 0fb9ed48c9 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	Squirrel/SQRLShipItLauncher.m
2014-01-06 12:04:39 +00:00
Keith Duncan 1db648059a Wrap the authorization in an Objective-C object for lifetime management 2014-01-06 12:01:12 +00:00
Keith Duncan fccf29122d Don’t __bridge cast the AuthorizationRef 2014-01-03 20:52:33 +00:00
Matt Diephouse 502c96a6df ++RAC 2013-12-30 14:04:02 -05:00
Josh Abernathy 90a6a2fe16 Merge pull request #94 from Squirrel/update-mantle-urls
Update URLs to Mantle repository
2013-12-13 14:57:10 -08:00
Justin Spahr-Summers c2869a05e9 Update URLs to Mantle repository
This change isn’t strictly necessary due to automatic redirection, but
let’s keep our links fresh.
2013-12-13 12:55:10 -08:00
Justin Spahr-Summers aa69586334 Merge pull request #90 from Squirrel/note-rac-dependency
Note that we're dependent on ReactiveCocoa and Mantle
2013-12-06 10:03:12 -08:00
Keith Duncan 8f3f5a5a72 Revert change to ReactiveCocoa pin 2013-12-06 12:53:55 +00:00
Keith Duncan f48ccd2a21 Float the version number dependency
Saves updating this when Squirrel switches to 3.0 too
2013-12-06 12:44:45 +00:00
Keith Duncan e21fd640b9 Commas-- 2013-12-05 14:14:01 +00:00
Keith Duncan 7bb7799b17 Change version dependency to 2.0 2013-12-05 14:13:33 +00:00
Keith Duncan 13a35d195b Clarify that the host application must embed the dependencies. 2013-12-04 12:28:13 +00:00
Keith Duncan 0c95e0c8c4 Note that we're dependent on ReactiveCocoa and Mantle
The client is responsible for including these because we don’t embed them.
2013-12-03 18:21:48 +00:00
Justin Spahr-Summers e5c65e530f Merge pull request #88 from Squirrel/skip-install
SKIP_INSTALL for ShipIt
2013-11-27 10:25:49 -08:00
Keith Duncan 65f191ce30 SKIP_INSTALL for ShipIt 2013-11-27 12:34:13 +00:00
12 changed files with 346 additions and 124 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/github/Mantle.git
url = https://github.com/MantleFramework/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
+25 -2
View File
@@ -23,13 +23,36 @@ updates installed are valid.
# Adopting Squirrel
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Run `script/bootstrap` from within the submodule
1. Add 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 Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
does not embed them itself.
# Dependencies
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
and [Mantle](https://github.com/MantleFramework/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
includes the directory that Squirrel.framework, ReactiveCocoa.framework
and Mantle.framework are copied into.
# Configuration
Once Squirrel is added to your project, you need to configure and start it.
+20
View File
@@ -15,6 +15,8 @@
534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; };
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; };
53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; };
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */ = {isa = PBXBuildFile; fileRef = 5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */; };
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */ = {isa = PBXBuildFile; fileRef = 5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */; };
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 53AACF4917E9CA0500B41027 /* SQRLUpdate.h */; settings = {ATTRIBUTES = (Public, ); }; };
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */; };
@@ -419,6 +421,8 @@
53710D8117D8F5C300A992DE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = "<group>"; };
53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = "<group>"; };
53710D8317D8F5C300A992DE /* xcodebuild.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = xcodebuild.awk; sourceTree = "<group>"; };
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLAuthorization.h; sourceTree = "<group>"; };
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLAuthorization.m; sourceTree = "<group>"; };
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
53AACF4917E9CA0500B41027 /* SQRLUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLUpdate.h; sourceTree = "<group>"; };
53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdate.m; sourceTree = "<group>"; };
@@ -622,6 +626,15 @@
path = script;
sourceTree = "<group>";
};
5374DCD8187AD0DC006B7056 /* Authorization */ = {
isa = PBXGroup;
children = (
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */,
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */,
);
name = Authorization;
sourceTree = "<group>";
};
53B70015181E944F000FFBD0 /* Products */ = {
isa = PBXGroup;
children = (
@@ -968,6 +981,7 @@
D0A56BF61804BABB00A84EDC /* Code Signing */,
D0A56BE71804BA2400A84EDC /* State */,
D06B58BC18032BE800656D97 /* Transactions */,
5374DCD8187AD0DC006B7056 /* Authorization */,
);
name = Shared;
path = Squirrel;
@@ -1016,6 +1030,7 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */,
D0964B3C17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.h in Headers */,
D0C22C4C179CC15100158214 /* SQRLUpdater.h in Headers */,
D0C22C52179CC23900158214 /* Squirrel.h in Headers */,
@@ -1441,6 +1456,7 @@
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */,
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1629,6 +1645,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Debug;
};
@@ -1638,6 +1655,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
@@ -1647,6 +1665,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Test;
};
@@ -1656,6 +1675,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Profile;
};
+27
View File
@@ -0,0 +1,27 @@
//
// SQRLAuthorization.h
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Wraps an AuthorizationRef.
@interface SQRLAuthorization : NSObject
// Designated initializer.
//
// authorization - Must not be NULL, the returned object assumes ownership of
// the passed authorization.
//
// Returns an object which ties the `authorization` argument to the returned
// object's lifetime.
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
@end
+28
View File
@@ -0,0 +1,28 @@
//
// SQRLAuthorization.m
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLAuthorization.h"
@implementation SQRLAuthorization
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
NSParameterAssert(authorization != NULL);
self = [self init];
if (self == nil) return nil;
_authorization = authorization;
return self;
}
- (void)dealloc {
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
}
@end
+1 -1
View File
@@ -544,7 +544,7 @@ typedef struct {
return YES;
}];
return [enumerator.rac_promise start];
return enumerator.allObjects.rac_signal;
}]
flattenMap:^(NSURL *URL) {
const char *path = URL.path.fileSystemRepresentation;
+8 -8
View File
@@ -13,6 +13,7 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
@@ -108,16 +109,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}]];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -127,11 +125,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, id authorization) {
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
@@ -140,7 +140,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
+2 -2
View File
@@ -104,8 +104,8 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
// other exit mechanism.
//
// After invoking this method, the receiver is responsible for terminating the
// application upon success. The app must not be terminated in any other way
// After subscribing to this signal, the receiver is responsible for terminating
// the application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
+113 -42
View File
@@ -32,6 +32,10 @@ const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@interface SQRLUpdater ()
// The code signature for the running application, used to check updates before
@@ -43,6 +47,16 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// 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.
@@ -151,13 +165,56 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
if (_signature == nil) {
#if DEBUG
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
return nil;
#else
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
#endif
}
@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.allObjects.rac_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);
}
}];
}]
collect]
// These operators ensure that the actual work only executes once,
// without starting it immediately.
concat:[RACSignal never]]
shareWhileActive]
take:1]
flattenMap:^(NSArray *URLs) {
return URLs.rac_signal;
}]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
_checkForUpdatesAction = [[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
flattenMap:^(NSURLRequest *request) {
@@ -169,7 +226,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
subject]
setNameWithFormat:@"%@ updates", self];
_shipItLauncher = [[[[RACSignal
_shipItLauncher = [[[[[[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
@@ -181,8 +238,13 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
// These operators ensure that the actual work only executes once,
// without starting it immediately. Super hacky.
concat:[RACSignal return:RACUnit.defaultUnit]]
concat:[RACSignal never]]
shareWhileActive]
take:1]
ignoreValues]
setNameWithFormat:@"shipItLauncher"];
return self;
@@ -203,7 +265,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
@strongify(self);
return [[[self.checkForUpdatesAction
deferred]
signalWithValue:nil]
ignoreValues]
catch:^(NSError *error) {
NSLog(@"Error checking for updates: %@", error);
@@ -223,38 +285,43 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:newRequest]
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
ignoreValues]
concat:[NSURLConnection rac_sendAsynchronousRequest:newRequest]]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
}
- (RACSignal *)updateFromJSONData:(NSData *)data {
@@ -361,7 +428,14 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
return [[SQRLZipArchiver
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
}]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
@@ -376,7 +450,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -410,8 +484,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
return YES;
}];
NSURL *updateBundleURL = [[[enumerator.rac_promise
start]
NSURL *updateBundleURL = [[enumerator.allObjects.rac_signal
filter:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
@@ -513,7 +586,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[[[SQRLShipItState
return [[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
@@ -531,8 +604,6 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// Never allow `completed` to escape this signal chain (in case
// -terminate: is asynchronous or something crazy).
concat:[RACSignal never]]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
+119 -66
View File
@@ -6,8 +6,10 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTestUpdate.h"
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
#import "SQRLTestUpdate.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
SpecBegin(SQRLUpdater)
@@ -50,89 +52,140 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should not install a corrupt update", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
it(@"should not install a corrupt update", ^{
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
NSRunningApplication *app = launchWithEnvironment(nil);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
NSURL *updateURL = [self createTestApplicationUpdate];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
writeUpdate(update);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.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", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
writeUpdate(update);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager applicationSupportURL] first];
expect(appSupportURL).notTo.beNil();
updateDirectoryURLs = [[RACSignal
defer:^{
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
if (contents == nil) return [RACSignal empty];
return contents.rac_signal;
}]
filter:^(NSURL *directoryURL) {
return [directoryURL.lastPathComponent hasPrefix:@"update."];
}];
});
it(@"should remove downloaded archives after updating", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
// Also test that this is the only update directory (any others
// should've been removed).
NSArray *directoryURLs = [updateDirectoryURLs array];
expect(directoryURLs.count).to.equal(1);
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
expect(contents).to.equal(@[]);
});
});
});
describe(@"response handling", ^{
@@ -155,7 +208,7 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
@@ -172,7 +225,7 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
+1 -1
View File
@@ -70,7 +70,7 @@
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
updateCheckCount++;
return [self.updater.checkForUpdatesAction deferred];
return [self.updater.checkForUpdatesAction signalWithValue:nil];
}]
doNext:^(SQRLDownloadedUpdate *update) {
NSLog(@"Got a candidate update: %@", update);