Compare commits
37
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33137bcae6 | ||
|
|
532c565b2a | ||
|
|
9f5a0761ae | ||
|
|
572ef9aa9b | ||
|
|
b8aef91738 | ||
|
|
36e0531218 | ||
|
|
77aec66456 | ||
|
|
e8523fd74d | ||
|
|
767892c788 | ||
|
|
34e37569d7 | ||
|
|
4c93cb4508 | ||
|
|
431224c788 | ||
|
|
ea8f805825 | ||
|
|
7bb4c131e1 | ||
|
|
372a7d783a | ||
|
|
b85e03fbe9 | ||
|
|
48455f1ee4 | ||
|
|
5cd5fdb2ed | ||
|
|
aa545ed419 | ||
|
|
3d507a58a6 | ||
|
|
406728c066 | ||
|
|
53adc17554 | ||
|
|
0fb9ed48c9 | ||
|
|
1db648059a | ||
|
|
fccf29122d | ||
|
|
502c96a6df | ||
|
|
90a6a2fe16 | ||
|
|
c2869a05e9 | ||
|
|
aa69586334 | ||
|
|
8f3f5a5a72 | ||
|
|
f48ccd2a21 | ||
|
|
e21fd640b9 | ||
|
|
7bb7799b17 | ||
|
|
13a35d195b | ||
|
|
0c95e0c8c4 | ||
|
|
e5c65e530f | ||
|
|
65f191ce30 |
+1
-1
@@ -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
|
||||
|
||||
Vendored
+1
-1
Submodule External/ReactiveCocoa updated: 0138dabe60...09ed195916
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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)];
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user