Compare commits
69
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0828897f1b | ||
|
|
ff31a5fcdd | ||
|
|
016cd25fda | ||
|
|
afebefc78a | ||
|
|
3c7916e02e | ||
|
|
8d6063d9a9 | ||
|
|
24c118fa1e | ||
|
|
ff7b9de34f | ||
|
|
5994cf9584 | ||
|
|
518ee666a3 | ||
|
|
73cc6802d3 | ||
|
|
1403a7ad81 | ||
|
|
24d9ef1f86 | ||
|
|
51bd593666 | ||
|
|
44a471b903 | ||
|
|
6e2f4faa3f | ||
|
|
43fd18e4c9 | ||
|
|
d08804ddce | ||
|
|
39a47ac14f | ||
|
|
49148013dc | ||
|
|
15c80b1cbf | ||
|
|
f89762550f | ||
|
|
b224b30a27 | ||
|
|
bfa660b5bc | ||
|
|
3ed1dc26fa | ||
|
|
442e51b89d | ||
|
|
a8b6a9f262 | ||
|
|
05019c8e82 | ||
|
|
d569a6524e | ||
|
|
5c64d501f4 | ||
|
|
ee8c781284 | ||
|
|
d8b1c4b6ca | ||
|
|
3582cdec0b | ||
|
|
ef169d19e9 | ||
|
|
582593dca7 | ||
|
|
ba6df2df14 | ||
|
|
65463d7612 | ||
|
|
1f38d749ea | ||
|
|
c4871aa371 | ||
|
|
f067d6b778 | ||
|
|
2c10c116e9 | ||
|
|
cfc2d55ca9 | ||
|
|
daaf62645e | ||
|
|
f34c02b79b | ||
|
|
5d5f5eaa12 | ||
|
|
40e5ac130e | ||
|
|
e449f18bd3 | ||
|
|
6d4f8a731b | ||
|
|
9debcdcd57 | ||
|
|
4d489e5add | ||
|
|
60f4089f6f | ||
|
|
33d2c31db9 | ||
|
|
267cd56bd6 | ||
|
|
2a4147f3b1 | ||
|
|
fb991f348b | ||
|
|
839f4a9708 | ||
|
|
e142e1bef0 | ||
|
|
14892783cd | ||
|
|
88c65394c1 | ||
|
|
afc0ad0b20 | ||
|
|
0c533da4d7 | ||
|
|
a5f8798ce8 | ||
|
|
ab52a12630 | ||
|
|
954de5d480 | ||
|
|
df71c4d256 | ||
|
|
d73df99d57 | ||
|
|
bfc2652481 | ||
|
|
90f31dd0f8 | ||
|
|
470338574f |
+6
-19
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
github "Mantle/Mantle" ~> 1.5.3
|
||||
github "ReactiveCocoa/ReactiveCocoa" ~> 2.4.3
|
||||
@@ -1,3 +0,0 @@
|
||||
github "jspahrsummers/xcconfigs" >= 0.7.1
|
||||
github "Quick/Quick"
|
||||
github "Quick/Nimble"
|
||||
@@ -1,5 +0,0 @@
|
||||
github "Mantle/Mantle" "1.5.4"
|
||||
github "Quick/Nimble" "v0.4.2"
|
||||
github "Quick/Quick" "v0.3.1"
|
||||
github "ReactiveCocoa/ReactiveCocoa" "v2.4.7"
|
||||
github "jspahrsummers/xcconfigs" "0.7.2"
|
||||
Vendored
-1
Submodule Carthage/Checkouts/Mantle deleted from d9dc95002c
Vendored
-1
Submodule Carthage/Checkouts/Nimble deleted from 8927113f87
Vendored
-1
Submodule Carthage/Checkouts/Quick deleted from 8bf96f7089
Vendored
-1
Submodule Carthage/Checkouts/ReactiveCocoa deleted from 54098f8b45
Vendored
-1
Submodule Carthage/Checkouts/xcconfigs deleted from 2e77204b59
+1
Submodule External/Mantle added at fe3a65987b
+1
Submodule External/ReactiveCocoa added at 16c181bbe8
@@ -39,7 +39,7 @@ 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.
|
||||
@@ -113,18 +113,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) {
|
||||
@@ -175,4 +169,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).
|
||||
|
||||
+164
-391
File diff suppressed because it is too large
Load Diff
+13
-12
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0620"
|
||||
LastUpgradeVersion = "0510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -15,8 +15,8 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D014AC0017B97885007D79D0"
|
||||
BuildableName = "ShipIt"
|
||||
BlueprintName = "ShipIt"
|
||||
BuildableName = "shipit-installer"
|
||||
BlueprintName = "ShipIt Installer"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
@@ -28,6 +28,16 @@
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Test">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
BuildableName = "SquirrelTests.xctest"
|
||||
BlueprintName = "SquirrelTests"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
@@ -39,15 +49,6 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D014AC0017B97885007D79D0"
|
||||
BuildableName = "ShipIt"
|
||||
BlueprintName = "ShipIt"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0620"
|
||||
version = "2.0">
|
||||
LastUpgradeVersion = "0510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -22,10 +22,10 @@
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
@@ -39,8 +39,8 @@
|
||||
<TestAction
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||
buildConfiguration = "Debug">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Test">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
@@ -53,42 +53,16 @@
|
||||
</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>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
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"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
|
||||
BuildableName = "Squirrel.framework"
|
||||
BlueprintName = "Squirrel"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "SQRLUpdateFromURL"
|
||||
@@ -105,15 +79,6 @@
|
||||
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 = "0620"
|
||||
LastUpgradeVersion = "0510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -48,8 +48,7 @@
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
|
||||
@@ -67,8 +66,7 @@
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
|
||||
|
||||
+8
-8
@@ -5,18 +5,18 @@
|
||||
location = "group:Squirrel.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/Mantle/Mantle.xcodeproj">
|
||||
location = "group:External/Mantle/Mantle.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/ReactiveCocoa/ReactiveCocoa.xcodeproj">
|
||||
location = "group:External/ReactiveCocoa/ReactiveCocoaFramework/ReactiveCocoa.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/ReactiveCocoa/external/specta/Specta.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/ReactiveCocoa/external/expecta/Expecta.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
|
||||
</FileRef>
|
||||
<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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
//
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@implementation SQRLDirectoryManager
|
||||
|
||||
@@ -24,7 +23,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 = NSProcessInfo.processInfo.processName;
|
||||
}
|
||||
|
||||
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
#pragma mark MTLJSONSerializing
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
+22
-84
@@ -7,24 +7,16 @@
|
||||
//
|
||||
|
||||
#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 "SQRLTerminationListener.h"
|
||||
#import <libkern/OSAtomic.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <sys/xattr.h>
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
|
||||
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
|
||||
@@ -172,7 +164,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
|
||||
return [self abortInstall];
|
||||
}];
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -262,49 +254,27 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
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]];
|
||||
return [[[[[[[self
|
||||
acquireTargetBundleURLForRequest:request]
|
||||
concat:[self installItemToURL:request.targetBundleURL fromURL:updateBundleURL]]
|
||||
concat:[RACSignal return:request.updateBundleURL]]
|
||||
concat:[RACSignal return:updateBundleURL]]
|
||||
concat:[RACSignal defer:^{
|
||||
return [RACSignal return:self.ownedBundle.temporaryURL];
|
||||
}]]
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [[[self
|
||||
deleteOwnedBundleAtURL:location]
|
||||
doError:^(NSError *error) {
|
||||
NSLog(@"Couldn't remove owned bundle at location %@, error %@", location, error.sqrl_verboseDescription);
|
||||
}]
|
||||
concat:[RACSignal return:request]];
|
||||
catchTo:[RACSignal empty]];
|
||||
}]
|
||||
doCompleted:^{
|
||||
self.ownedBundle = nil;
|
||||
@@ -435,36 +405,6 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
|
||||
#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;
|
||||
|
||||
// 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];
|
||||
}];
|
||||
}
|
||||
|
||||
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
NSParameterAssert(targetURL != nil);
|
||||
NSParameterAssert(sourceURL != nil);
|
||||
@@ -477,7 +417,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 +469,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);
|
||||
|
||||
@@ -585,9 +525,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
return NO;
|
||||
}];
|
||||
|
||||
return [[enumerator.rac_sequence.signal
|
||||
startWith:directoryURL]
|
||||
subscribe:subscriber];
|
||||
return [enumerator.rac_sequence.signal subscribe:subscriber];
|
||||
}]
|
||||
flattenMap:^(NSURL *itemURL) {
|
||||
return [[[self
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
|
||||
#import "EXTKeyPathCoding.h"
|
||||
#import <ReactiveCocoa/EXTKeyPathCoding.h>
|
||||
|
||||
@implementation SQRLInstallerOwnedBundle
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// SQRLShipItConnection.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-08-12.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class RACSignal;
|
||||
@class SQRLShipItRequest;
|
||||
|
||||
// The domain for errors originating within SQRLShipItConnection.
|
||||
extern NSString * const SQRLShipItConnectionErrorDomain;
|
||||
|
||||
// The ShipIt service could not be started.
|
||||
extern const NSInteger SQRLShipItConnectionErrorCouldNotStartService;
|
||||
|
||||
// Installing an update requires the coordination of multiple processes,
|
||||
// `SQRLShipItConnection` is responsible for submitting the launchd jobs
|
||||
// required to perform an installation request.
|
||||
//
|
||||
// The multiprocess approach to waiting for termination and installing is
|
||||
// designed to keep uses of AppKit API (i.e. `NSRunningApplication`) out of the
|
||||
// installer process - which may be running in the root bootstrap context - and
|
||||
// put them in a process running in the user bootstrap context. These processes
|
||||
// can then communicate using the file system as a message bus.
|
||||
//
|
||||
// Frustratingly although LaunchServices is daemon and root safe since 10.5, the
|
||||
// LaunchServices API for querying the running applications isn't public
|
||||
// (internally `NSRunningApplication` uses this LaunchServices private API,
|
||||
// though of course this is subject to change and `NSRunningApplication` could
|
||||
// also do other non-root safe things).
|
||||
//
|
||||
// When a user application running Squirrel wants to install an update, Squirrel
|
||||
// submits two launchd jobs, one which will wait for application termination and
|
||||
// write an empty file to the given location when the criteria are met, and the
|
||||
// other which will wait for that file to appear and then perform the install as
|
||||
// before. The "wait for termination" job is always submitted to the user
|
||||
// domain, but the installer job is submitted to a domain based on whether the
|
||||
// install location is writable by the current user.
|
||||
//
|
||||
// To perform the relaunch when the install is complete, it is safe to use the
|
||||
// LaunchServices API from the installer process, the app will be launched in
|
||||
// the user's GUI session.
|
||||
//
|
||||
// From <https://developer.apple.com/library/mac/technotes/tn2083/>
|
||||
//
|
||||
// > If the EUID of the calling process is zero, the application is launched in
|
||||
// > the context of the currently active GUI login session. If there is no
|
||||
// > currently active GUI login session (no one is logged in, or a logged in
|
||||
// > user has fast user switched to the login window), the behavior is
|
||||
// > unspecified (r. 5321293).
|
||||
@interface SQRLShipItConnection : NSObject
|
||||
|
||||
// Returns the label for the ShipIt launchd job.
|
||||
+ (NSString *)shipItInstallerJobLabel;
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// privileged - Determines which launchd domain to launch the job in.
|
||||
// If YES, ShipIt is launched in the root domain, otherwise it is
|
||||
// launched in the current user’s domain.
|
||||
//
|
||||
// Returns an initialised connection which can be used to start an install.
|
||||
- (instancetype)initWithRootPrivileges:(BOOL)rootPrivileges;
|
||||
|
||||
// Submits the request to the process network which will wait for termination
|
||||
// and then install.
|
||||
//
|
||||
// After sending the request, the update will be attempted when the sending
|
||||
// process terminates.
|
||||
//
|
||||
// request - The install parameters, target bundle, update bundle, whether to
|
||||
// launch when install is complete etc. Must not be nil.
|
||||
//
|
||||
// Returns a signal which will complete, or error, on a background scheduler.
|
||||
- (RACSignal *)sendRequest:(SQRLShipItRequest *)request;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// SQRLShipItConnection.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-08-12.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLShipItConnection.h"
|
||||
#import "EXTScope.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Security/Security.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <launch.h>
|
||||
#import "SQRLAuthorization.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
NSString * const SQRLShipItConnectionErrorDomain = @"SQRLShipItConnectionErrorDomain";
|
||||
|
||||
const NSInteger SQRLShipItConnectionErrorCouldNotStartService = 1;
|
||||
|
||||
@interface SQRLShipItConnection ()
|
||||
@property (readonly, nonatomic, assign) BOOL privileged;
|
||||
@end
|
||||
|
||||
@implementation SQRLShipItConnection
|
||||
|
||||
+ (NSString *)shipItInstallerJobLabel {
|
||||
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
|
||||
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
|
||||
}
|
||||
|
||||
+ (NSMutableDictionary *)jobDictionaryWithLabel:(NSString *)jobLabel executableName:(NSString *)executableName arguments:(NSArray *)arguments {
|
||||
NSParameterAssert(jobLabel != nil);
|
||||
NSParameterAssert(executableName != nil);
|
||||
NSParameterAssert(arguments != nil);
|
||||
|
||||
NSMutableDictionary *jobDict = [NSMutableDictionary dictionary];
|
||||
jobDict[@(LAUNCH_JOBKEY_LABEL)] = jobLabel;
|
||||
jobDict[@(LAUNCH_JOBKEY_NICE)] = @(-1);
|
||||
jobDict[@(LAUNCH_JOBKEY_ENABLETRANSACTIONS)] = @NO;
|
||||
jobDict[@(LAUNCH_JOBKEY_THROTTLEINTERVAL)] = @2;
|
||||
|
||||
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
|
||||
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
|
||||
|
||||
NSMutableArray *fullArguments = [NSMutableArray arrayWithObject:[squirrelBundle pathForResource:executableName ofType:nil]];
|
||||
[fullArguments addObjectsFromArray:arguments];
|
||||
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = fullArguments;
|
||||
|
||||
return jobDict;
|
||||
}
|
||||
|
||||
+ (RACSignal *)shipItInstallerJobDictionary {
|
||||
NSString *jobLabel = self.shipItInstallerJobLabel;
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
map:^(NSURL *appSupportURL) {
|
||||
NSMutableArray *arguments = [[NSMutableArray alloc] init];
|
||||
|
||||
// Pass in the service name so ShipIt knows how to broadcast itself.
|
||||
[arguments addObject:jobLabel];
|
||||
|
||||
NSMutableDictionary *jobDict = [self jobDictionaryWithLabel:jobLabel executableName:@"shipit-installer" arguments:arguments];
|
||||
jobDict[@(LAUNCH_JOBKEY_KEEPALIVE)] = @{
|
||||
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
|
||||
};
|
||||
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
|
||||
|
||||
return jobDict;
|
||||
}]
|
||||
setNameWithFormat:@"+shipItInstallerJobDictionary"];
|
||||
}
|
||||
|
||||
+ (RACSignal *)shipItAuthorization {
|
||||
return [[RACSignal
|
||||
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
|
||||
AuthorizationItem rightItems[] = {
|
||||
{
|
||||
.name = kSMRightModifySystemDaemons,
|
||||
},
|
||||
};
|
||||
|
||||
AuthorizationRights rights = {
|
||||
.count = sizeof(rightItems) / sizeof(*rightItems),
|
||||
.items = rightItems,
|
||||
};
|
||||
|
||||
NSString *prompt = NSLocalizedString(@"An update is ready to install.", @"SQRLShipItConnection, launch shipit, authorization prompt");
|
||||
|
||||
NSString *iconName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
|
||||
NSString *iconPath = (iconName == nil ? nil : [NSBundle.mainBundle pathForImageResource:iconName]);
|
||||
|
||||
AuthorizationItem environmentItems[] = {
|
||||
{
|
||||
.name = kAuthorizationEnvironmentPrompt,
|
||||
.valueLength = strlen(prompt.UTF8String),
|
||||
.value = (void *)prompt.UTF8String,
|
||||
},
|
||||
{
|
||||
.name = kAuthorizationEnvironmentIcon,
|
||||
.valueLength = (iconPath == nil ? 0 : strlen(iconPath.UTF8String)),
|
||||
.value = (void *)iconPath.UTF8String,
|
||||
},
|
||||
};
|
||||
|
||||
AuthorizationEnvironment environment = {
|
||||
.count = sizeof(environmentItems) / sizeof(*environmentItems),
|
||||
.items = environmentItems,
|
||||
};
|
||||
|
||||
AuthorizationRef authorization = NULL;
|
||||
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
|
||||
|
||||
if (authorizationError == noErr) {
|
||||
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
|
||||
[subscriber sendCompleted];
|
||||
} else {
|
||||
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}]
|
||||
setNameWithFormat:@"+shipItAuthorization"];
|
||||
}
|
||||
|
||||
- (instancetype)initWithRootPrivileges:(BOOL)rootPrivileges {
|
||||
self = [self init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_privileged = rootPrivileges;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (RACSignal *)sendRequest:(SQRLShipItRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
return [[[self
|
||||
submitInstallerJobForRequestIfNeeded:request]
|
||||
concat:[RACSignal defer:^{
|
||||
|
||||
}]]
|
||||
setNameWithFormat:@"%@ -sendRequest: %@", self, request];
|
||||
}
|
||||
|
||||
- (RACSignal *)submitInstallerJobForRequestIfNeeded:(SQRLShipItRequest *)request {
|
||||
// TODO implement lazy submission when the job is already loaded in launchd
|
||||
|
||||
CFStringRef domain = NULL; RACSignal *authorization;
|
||||
if (self.privileged) {
|
||||
domain = kSMDomainSystemLaunchd;
|
||||
authorization = self.class.shipItAuthorization;
|
||||
} else {
|
||||
domain = kSMDomainUserLaunchd;
|
||||
authorization = [RACSignal return:nil];
|
||||
}
|
||||
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
self.class.shipItInstallerJobDictionary,
|
||||
authorization,
|
||||
] reduce:^(NSDictionary *job, SQRLAuthorization *authorization) {
|
||||
return [self submitJob:job domain:(__bridge id)domain authorization:authorization];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -submitInstallerJobForRequestIfNeeded: %@", self, request];
|
||||
}
|
||||
|
||||
- (RACSignal *)submitJob:(NSDictionary *)job domain:(NSString *)domain authorization:(SQRLAuthorization *)authorizationValue {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSString *jobLabel = job[@(LAUNCH_JOBKEY_LABEL)];
|
||||
|
||||
AuthorizationRef authorization = authorizationValue.authorization;
|
||||
|
||||
CFErrorRef cfError;
|
||||
if (!SMJobRemove((__bridge CFStringRef)domain, (__bridge CFStringRef)jobLabel, authorization, true, &cfError)) {
|
||||
NSError *error = CFBridgingRelease(cfError);
|
||||
cfError = NULL;
|
||||
|
||||
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
|
||||
NSLog(@"Could not remove previous ShipIt job %@: %@", jobLabel, error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!SMJobSubmit((__bridge CFStringRef)domain, (__bridge CFDictionaryRef)job, authorization, &cfError)) {
|
||||
return [RACSignal error:CFBridgingRelease(cfError)];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -submitJob: %@ domain: %@ authorization: %@", self, job, domain, authorizationValue];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// SQRLShipItLauncher.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-08-12.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// The domain for errors originating within SQRLShipItLauncher.
|
||||
extern NSString * const SQRLShipItLauncherErrorDomain;
|
||||
|
||||
// The ShipIt service could not be started.
|
||||
extern const NSInteger SQRLShipItLauncherErrorCouldNotStartService;
|
||||
|
||||
// Responsible for launching the ShipIt service to actually install an update.
|
||||
@interface SQRLShipItLauncher : NSObject
|
||||
|
||||
// Returns the label for the ShipIt launchd job.
|
||||
+ (NSString *)shipItJobLabel;
|
||||
|
||||
// Attempts to launch ShipIt.
|
||||
//
|
||||
// privileged - Determines which launchd domain to launch the job in.
|
||||
// If YES, ShipIt is launched in the root domain, otherwise it is
|
||||
// launched in the current user’s domain.
|
||||
//
|
||||
// Returns a signal which will complete, or error, on a background scheduler.
|
||||
+ (RACSignal *)launchPrivileged:(BOOL)privileged;
|
||||
|
||||
@end
|
||||
@@ -1,150 +0,0 @@
|
||||
//
|
||||
// SQRLShipItLauncher.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-08-12.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "EXTScope.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Security/Security.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <launch.h>
|
||||
#import "SQRLAuthorization.h"
|
||||
|
||||
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
|
||||
|
||||
const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
|
||||
@implementation SQRLShipItLauncher
|
||||
|
||||
+ (NSString *)shipItJobLabel {
|
||||
return [SQRLDirectoryManager.currentApplicationManager.applicationIdentifier stringByAppendingString:@".ShipIt"];
|
||||
}
|
||||
|
||||
+ (RACSignal *)shipItJobDictionary {
|
||||
NSString *jobLabel = self.shipItJobLabel;
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
map:^(NSURL *appSupportURL) {
|
||||
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
|
||||
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
|
||||
|
||||
NSMutableDictionary *jobDict = [NSMutableDictionary dictionary];
|
||||
jobDict[@(LAUNCH_JOBKEY_LABEL)] = jobLabel;
|
||||
jobDict[@(LAUNCH_JOBKEY_NICE)] = @(-1);
|
||||
jobDict[@(LAUNCH_JOBKEY_ENABLETRANSACTIONS)] = @NO;
|
||||
jobDict[@(LAUNCH_JOBKEY_THROTTLEINTERVAL)] = @2;
|
||||
jobDict[@(LAUNCH_JOBKEY_KEEPALIVE)] = @{
|
||||
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
|
||||
};
|
||||
|
||||
jobDict[@(LAUNCH_JOBKEY_MACHSERVICES)] = @{
|
||||
jobLabel: @YES
|
||||
};
|
||||
|
||||
NSMutableArray *arguments = [[NSMutableArray alloc] init];
|
||||
[arguments addObject:[squirrelBundle URLForResource:@"ShipIt" withExtension:nil].path];
|
||||
|
||||
// Pass in the service name so ShipIt knows how to broadcast itself.
|
||||
[arguments addObject:jobLabel];
|
||||
|
||||
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = arguments;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
|
||||
|
||||
return jobDict;
|
||||
}]
|
||||
setNameWithFormat:@"+shipItJobDictionary"];
|
||||
}
|
||||
|
||||
+ (RACSignal *)shipItAuthorization {
|
||||
return [[RACSignal
|
||||
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
|
||||
AuthorizationItem rightItems[] = {
|
||||
{
|
||||
.name = kSMRightModifySystemDaemons,
|
||||
},
|
||||
};
|
||||
|
||||
AuthorizationRights rights = {
|
||||
.count = sizeof(rightItems) / sizeof(*rightItems),
|
||||
.items = rightItems,
|
||||
};
|
||||
|
||||
NSString *prompt = NSLocalizedString(@"An update is ready to install.", @"SQRLShipItLauncher, launch shipit, authorization prompt");
|
||||
|
||||
NSString *iconName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
|
||||
NSString *iconPath = (iconName == nil ? nil : [NSBundle.mainBundle.resourceURL URLByAppendingPathComponent:iconName].path);
|
||||
|
||||
AuthorizationItem environmentItems[] = {
|
||||
{
|
||||
.name = kAuthorizationEnvironmentPrompt,
|
||||
.valueLength = strlen(prompt.UTF8String),
|
||||
.value = (void *)prompt.UTF8String,
|
||||
},
|
||||
{
|
||||
.name = kAuthorizationEnvironmentIcon,
|
||||
.valueLength = iconPath == nil ? 0 : strlen(iconPath.UTF8String),
|
||||
.value = (void *)iconPath.UTF8String,
|
||||
},
|
||||
};
|
||||
|
||||
AuthorizationEnvironment environment = {
|
||||
.count = sizeof(environmentItems) / sizeof(*environmentItems),
|
||||
.items = environmentItems,
|
||||
};
|
||||
|
||||
AuthorizationRef authorization = NULL;
|
||||
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
|
||||
|
||||
if (authorizationError == noErr) {
|
||||
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
|
||||
[subscriber sendCompleted];
|
||||
} else {
|
||||
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}]
|
||||
setNameWithFormat:@"+shipItAuthorization"];
|
||||
}
|
||||
|
||||
+ (RACSignal *)launchPrivileged:(BOOL)privileged {
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
self.shipItJobDictionary,
|
||||
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
|
||||
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
|
||||
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
|
||||
|
||||
AuthorizationRef authorization = authorizationValue.authorization;
|
||||
|
||||
CFErrorRef cfError;
|
||||
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
|
||||
NSError *error = CFBridgingRelease(cfError);
|
||||
cfError = NULL;
|
||||
|
||||
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
|
||||
NSLog(@"Could not remove previous ShipIt job: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
|
||||
return [RACSignal error:CFBridgingRelease(cfError)];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"+launchPrivileged: %i", (int)privileged];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -42,24 +42,21 @@ extern NSString * const SQRLShipItRequestPropertyErrorKey;
|
||||
// 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.
|
||||
// Reads a `SQRLShipItState` from disk, at the location specified by the URL.
|
||||
//
|
||||
// URLSignal - Determines the file location to read from, the signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
// URL - The file location to read from. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
|
||||
// complete, or error.
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URLSignal;
|
||||
+ (RACSignal *)readFromURL:(NSURL *)URL;
|
||||
|
||||
// Writes the receiver to disk, at the location specified by the given URL
|
||||
// signal.
|
||||
// Reads a `SQRLShipItState` from encoded data.
|
||||
//
|
||||
// URLSignal - Determines the file location to write to. The signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
// data - Serialised request from `serialization`.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URLSignal;
|
||||
// Returns a signal which decodes the serialisation and sends a
|
||||
// `SQRLShipItRequest` then completes, or errors.
|
||||
+ (RACSignal *)readFromData:(NSData *)data;
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
@@ -73,11 +70,10 @@ extern NSString * const SQRLShipItRequestPropertyErrorKey;
|
||||
// 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;
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation;
|
||||
|
||||
// The URL to the downloaded update's app bundle.
|
||||
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
|
||||
@@ -94,7 +90,17 @@ extern NSString * const SQRLShipItRequestPropertyErrorKey;
|
||||
// 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;
|
||||
// Writes the receiver's serialization to disk, at the location specified by the
|
||||
// URL.
|
||||
//
|
||||
// URL - The file location to write to. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)writeToURL:(NSURL *)URL;
|
||||
|
||||
// Encode the receiver for saving to disk or sending over IPC.
|
||||
//
|
||||
// Returns a signal which sends a `NSData` then completes, or errors.
|
||||
- (RACSignal *)serialization;
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
#import "EXTKeyPathCoding.h"
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
NSString * const SQRLShipItRequestErrorDomain = @"SQRLShipItRequestErrorDomain";
|
||||
|
||||
@@ -47,13 +46,12 @@ NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestProperty
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation useUpdateBundleName:(BOOL)useUpdateBundleName {
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.updateBundleURL): updateBundleURL,
|
||||
@keypath(self.targetBundleURL): targetBundleURL,
|
||||
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
|
||||
@keypath(self.launchAfterInstallation): @(launchAfterInstallation),
|
||||
@keypath(self.useUpdateBundleName): @(useUpdateBundleName),
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
@@ -71,13 +69,13 @@ NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestProperty
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
|
||||
+ (RACSignal *)readFromURL:(NSURL *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[[URL
|
||||
flattenMap:^(NSURL *stateURL) {
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
NSData *data = [self readFromURL:stateURL error:&error];
|
||||
NSData *data = [self readFromURL:URL error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
@@ -132,23 +130,20 @@ NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestProperty
|
||||
setNameWithFormat:@"+readFromData: <NSData %p>", data];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
|
||||
- (RACSignal *)writeToURL:(NSURL *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
zip:@[
|
||||
URL,
|
||||
[self serialization]
|
||||
] reduce:^(NSURL *stateURL, NSData *data) {
|
||||
return [[[[self
|
||||
serialization]
|
||||
flattenMap:^(NSData *data) {
|
||||
NSError *error;
|
||||
BOOL write = [self writeData:data toURL:stateURL error:&error];
|
||||
BOOL write = [self writeData:data toURL:URL 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),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// SQRLTerminationListener.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-04.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// Waits for the termination of a GUI application.
|
||||
@interface SQRLTerminationListener : NSObject
|
||||
|
||||
// Initializes the receiver to wait for termination of the app at the given
|
||||
// location.
|
||||
//
|
||||
// bundleURL - The URL to the application bundle to watch. This must not be nil.
|
||||
// bundleID - The identifier of the application bundle to watch. This must not
|
||||
// be nil.
|
||||
- (id)initWithURL:(NSURL *)bundleURL bundleIdentifier:(NSString *)bundleID;
|
||||
|
||||
// Lazily waits for termination of all instances of the application identified
|
||||
// at initialization.
|
||||
//
|
||||
// Returns a signal which send an `NSRunningApplication` for each instance of the
|
||||
// application that is being watched (before the instance terminates), then
|
||||
// completes on a background scheduler.
|
||||
- (RACSignal *)waitForTermination;
|
||||
|
||||
@end
|
||||
@@ -1,95 +0,0 @@
|
||||
//
|
||||
// SQRLTerminationListener.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-04.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#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>
|
||||
|
||||
@interface SQRLTerminationListener ()
|
||||
|
||||
@property (nonatomic, copy, readonly) NSURL *bundleURL;
|
||||
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
|
||||
|
||||
// Waits for the process identified by the given PID to terminate.
|
||||
//
|
||||
// Returns a signal which sends `processIdentifier` as soon as the process is
|
||||
// being monitored, then completes once it exits, all on a background thread.
|
||||
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLTerminationListener
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithURL:(NSURL *)bundleURL bundleIdentifier:(NSString *)bundleID {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
NSParameterAssert(bundleID != nil);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_bundleURL = bundleURL.URLByStandardizingPath;
|
||||
_bundleIdentifier = [bundleID copy];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Termination Listening
|
||||
|
||||
- (RACSignal *)waitForTermination {
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
|
||||
return [apps.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
|
||||
}]
|
||||
filter:^(NSRunningApplication *application) {
|
||||
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
|
||||
}]
|
||||
flattenMap:^(NSRunningApplication *application) {
|
||||
return [[self
|
||||
waitForTerminationOfProcessIdentifier:application.processIdentifier]
|
||||
mapReplace:application];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -waitForTermination", self];
|
||||
}
|
||||
|
||||
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
|
||||
return [[RACSignal
|
||||
createSignal:^(id<RACSubscriber> subscriber) {
|
||||
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, processIdentifier, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
|
||||
|
||||
dispatch_source_set_registration_handler(source, ^{
|
||||
[subscriber sendNext:@(processIdentifier)];
|
||||
});
|
||||
|
||||
dispatch_source_set_event_handler(source, ^{
|
||||
[subscriber sendCompleted];
|
||||
});
|
||||
|
||||
dispatch_resume(source);
|
||||
return [RACDisposable disposableWithBlock:^{
|
||||
dispatch_source_cancel(source);
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
|
||||
}
|
||||
|
||||
#pragma mark NSObject
|
||||
|
||||
- (NSString *)description {
|
||||
return [NSString stringWithFormat:@"<%@: %p>{ bundleURL: %@, bundleIdentifier: %@ }", self.class, self, self.bundleURL, self.bundleIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,9 +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.
|
||||
|
||||
+73
-77
@@ -14,7 +14,8 @@
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItConnection.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLUpdate.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
@@ -44,10 +45,18 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
// sending them to ShipIt.
|
||||
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
|
||||
|
||||
// Lazily launches ShipIt upon first subscription.
|
||||
// When executed with an `SQRLShipItState`, launches ShipIt.
|
||||
@property (nonatomic, strong, readonly) RACCommand *shipItLauncher;
|
||||
|
||||
// Lazily removes outdated temporary directories (used for previous updates)
|
||||
// upon first subscription.
|
||||
//
|
||||
// Sends completed or error.
|
||||
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
|
||||
// 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.
|
||||
//
|
||||
@@ -159,6 +168,33 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
|
||||
@weakify(self);
|
||||
|
||||
_prunedUpdateDirectories = [[[[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *appSupportURL) {
|
||||
NSFileManager *manager = [[NSFileManager alloc] init];
|
||||
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:appSupportURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
|
||||
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, appSupportURL, error);
|
||||
return YES;
|
||||
}];
|
||||
|
||||
return [[enumerator.rac_sequence.signal
|
||||
filter:^(NSURL *enumeratedURL) {
|
||||
NSString *name = enumeratedURL.lastPathComponent;
|
||||
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
|
||||
}]
|
||||
doNext:^(NSURL *directoryURL) {
|
||||
NSError *error = nil;
|
||||
if (![manager removeItemAtURL:directoryURL error:&error]) {
|
||||
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
replayLazily]
|
||||
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
|
||||
|
||||
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(self.updateRequest != nil);
|
||||
@@ -168,8 +204,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
// Prune old updates before the first update check.
|
||||
return [[[[[[[[[self
|
||||
pruneUpdateDirectories]
|
||||
return [[[[[[[[self.prunedUpdateDirectories
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error pruning old updates: %@", error);
|
||||
return [RACSignal empty];
|
||||
@@ -221,20 +256,19 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
deliverOn:RACScheduler.mainThreadScheduler];
|
||||
}];
|
||||
|
||||
_shipItLauncher = [[[RACSignal
|
||||
defer:^{
|
||||
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
|
||||
_shipItLauncher = [[RACCommand alloc] initWithSignalBlock:^(SQRLShipItRequest *request) {
|
||||
NSURL *targetURL = request.targetBundleURL;
|
||||
|
||||
NSNumber *targetWritable = nil;
|
||||
NSError *targetWritableError = nil;
|
||||
BOOL gotWritable = [targetURL getResourceValue:&targetWritable forKey:NSURLIsWritableKey error:&targetWritableError];
|
||||
NSNumber *targetWritable = nil;
|
||||
NSError *targetWritableError = nil;
|
||||
BOOL gotWritable = [targetURL getResourceValue:&targetWritable forKey:NSURLIsWritableKey error:&targetWritableError];
|
||||
|
||||
// If we can't determine whether it can be written, assume nonprivileged and
|
||||
// wait for another, more canonical error.
|
||||
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
|
||||
}]
|
||||
replayLazily]
|
||||
setNameWithFormat:@"shipItLauncher"];
|
||||
// If we can't determine whether it can be written, assume
|
||||
// nonprivileged and wait for another, more canonical error.
|
||||
SQRLShipItConnection *connection = [[SQRLShipItConnection alloc] initWithRootPrivileges:(gotWritable && !targetWritable.boolValue)];
|
||||
|
||||
return [connection sendRequest:request];
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -384,7 +418,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *appSupportURL) {
|
||||
@@ -458,48 +492,8 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
}
|
||||
|
||||
- (RACSignal *)shipItStateURL {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return directoryManager.shipItStateURL;
|
||||
}]
|
||||
setNameWithFormat:@"%@ -shipItStateURL", self];
|
||||
}
|
||||
|
||||
/// 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 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);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
|
||||
return directoryManager.shipItStateURL;
|
||||
}
|
||||
|
||||
#pragma mark Installing Updates
|
||||
@@ -528,30 +522,32 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
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];
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO useUpdateBundleName:useUpdateBundleName];
|
||||
return [request writeUsingURL:self.shipItStateURL];
|
||||
return [[self.shipItStateURL
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [request writeToURL:location];
|
||||
}]
|
||||
concat:[RACSignal return:request]];
|
||||
}]
|
||||
then:^{
|
||||
return self.shipItLauncher;
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [self.shipItLauncher execute:request];
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing update", nil) description:NSLocalizedString(@"An update for %@ is being prepared. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier]
|
||||
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
|
||||
}
|
||||
|
||||
- (RACSignal *)relaunchToInstallUpdate {
|
||||
return [[[[[[[[SQRLShipItRequest
|
||||
readUsingURL:self.shipItStateURL]
|
||||
map:^(SQRLShipItRequest *request) {
|
||||
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES useUpdateBundleName:request.useUpdateBundleName];
|
||||
}]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [[request
|
||||
writeUsingURL:self.shipItStateURL]
|
||||
return [[[[[[self.shipItStateURL
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [[[[SQRLShipItRequest
|
||||
readFromURL:location]
|
||||
map:^(SQRLShipItRequest *request) {
|
||||
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES];
|
||||
}]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [request writeToURL:location];
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
|
||||
}]
|
||||
deliverOn:RACScheduler.mainThreadScheduler]
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
//
|
||||
// main.m
|
||||
// shipit
|
||||
//
|
||||
// Created by Alan Rogers on 29/07/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "EXTScope.h"
|
||||
#import <ReactiveCocoa/RACCommand.h>
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/RACScheduler.h>
|
||||
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import "RACSignal+SQRLTransactionExtensions.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLTerminationListener.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
// The maximum number of times ShipIt should run the same installation state, in
|
||||
// an attempt to update.
|
||||
//
|
||||
// If ShipIt is launched in the same state more than this number of times,
|
||||
// updating will abort.
|
||||
static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
|
||||
|
||||
// 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) {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
if (request.bundleIdentifier == nil) return [RACSignal empty];
|
||||
|
||||
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:request.targetBundleURL bundleIdentifier:request.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:nil error:&error]) {
|
||||
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"Application launched at %@", bundleURL);
|
||||
}];
|
||||
}
|
||||
|
||||
return action;
|
||||
}]
|
||||
subscribeError:^(NSError *error) {
|
||||
NSLog(@"Installation error: %@", error);
|
||||
exit(EXIT_FAILURE);
|
||||
} completed:^{
|
||||
exit(EXIT_SUCCESS);
|
||||
}];
|
||||
}
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
atexit_b(^{
|
||||
NSLog(@"ShipIt quitting");
|
||||
});
|
||||
|
||||
if (argc < 2) {
|
||||
NSLog(@"Missing launchd job label for ShipIt");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char const *jobLabel = argv[1];
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
|
||||
|
||||
installRequest([SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL], directoryManager);
|
||||
|
||||
dispatch_main();
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
//
|
||||
// main.m
|
||||
// shipit
|
||||
//
|
||||
// Created by Alan Rogers on 29/07/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import "RACSignal+SQRLTransactionExtensions.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
// The domain for errors generated here.
|
||||
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
|
||||
|
||||
typedef NS_ENUM(NSInteger, SQRLShipItError) {
|
||||
SQRLShipItErrorUnknownAction,
|
||||
SQRLShipItErrorMissingRequestData,
|
||||
};
|
||||
|
||||
static void reply(xpc_object_t response) {
|
||||
xpc_connection_t connection = xpc_dictionary_get_remote_connection(response);
|
||||
xpc_connection_send_message(connection, response);
|
||||
}
|
||||
|
||||
static void replyWithError(xpc_object_t response, NSError *error) {
|
||||
xpc_dictionary_set_bool(response, "success", false);
|
||||
|
||||
if (error != nil) {
|
||||
xpc_object_t errorDictionary = xpc_dictionary_create(NULL, NULL, 0);
|
||||
xpc_dictionary_set_string(errorDictionary, "domain", error.domain.UTF8String);
|
||||
xpc_dictionary_set_int64(errorDictionary, "code", error.code);
|
||||
xpc_dictionary_set_string(errorDictionary, "description", error.localizedDescription.UTF8String);
|
||||
xpc_dictionary_set_string(errorDictionary, "failureReason", error.localizedFailureReason.UTF8String);
|
||||
xpc_dictionary_set_string(errorDictionary, "recoverySuggestion", error.localizedRecoverySuggestion.UTF8String);
|
||||
xpc_dictionary_set_value(response, "error", errorDictionary);
|
||||
xpc_release(errorDictionary);
|
||||
}
|
||||
|
||||
reply(response);
|
||||
}
|
||||
|
||||
static void replyWithSuccess(xpc_object_t response) {
|
||||
xpc_dictionary_set_bool(response, "success", true);
|
||||
reply(response);
|
||||
}
|
||||
|
||||
// Client requests from peer connections.
|
||||
//
|
||||
// applicationIdentifier - Current process reverse DNS identifier.
|
||||
// request - XPC dictionary request object.
|
||||
//
|
||||
// Returns nothing.
|
||||
static void handleRequest(NSString *applicationIdentifier, xpc_object_t request) {
|
||||
xpc_object_t response = xpc_dictionary_create_reply(request);
|
||||
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
|
||||
|
||||
char const *action = xpc_dictionary_get_string(request, "action");
|
||||
if (strcmp(action, "install") != 0) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot process \"%s\" requests", action],
|
||||
};
|
||||
replyWithError(response, [NSError errorWithDomain:SQRLShipItErrorDomain code:SQRLShipItErrorUnknownAction userInfo:errorInfo]);
|
||||
return;
|
||||
}
|
||||
|
||||
size_t length;
|
||||
void const *requestBytes = xpc_dictionary_get_data(request, "request", &length);
|
||||
if (requestBytes == NULL) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot process \"%s\" requests without \"request\" data", action],
|
||||
};
|
||||
replyWithError(response, [NSError errorWithDomain:SQRLShipItErrorDomain code:SQRLShipItErrorUnknownAction userInfo:errorInfo]);
|
||||
return;
|
||||
}
|
||||
|
||||
[[[SQRLShipItRequest
|
||||
readFromData:[NSData dataWithBytes:requestBytes length:length] ]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [[installer.installUpdateCommand
|
||||
execute:request]
|
||||
catch:^(NSError *error) {
|
||||
return [[[installer.abortInstallationCommand
|
||||
execute:nil]
|
||||
doCompleted:^{
|
||||
NSLog(@"Abort completed successfully");
|
||||
}]
|
||||
concat:[RACSignal error:error]];
|
||||
}];
|
||||
}]
|
||||
subscribeError:^(NSError *error){
|
||||
NSLog(@"Installation failed with error: %@ %@", error, error.userInfo);
|
||||
replyWithError(response, error);
|
||||
} completed:^{
|
||||
NSLog(@"Installation completed successfully");
|
||||
replyWithSuccess(response);
|
||||
}];
|
||||
}
|
||||
|
||||
// Peer connections from the listener connection.
|
||||
//
|
||||
// applicationIdentifier - Current process reverse DNS identifier.
|
||||
// newConnection - The newly accepted connection from the listener.
|
||||
//
|
||||
// Returns nothing.
|
||||
static void handleConnection(NSString *applicationIdentifier, xpc_connection_t newConnection) {
|
||||
xpc_connection_set_event_handler(newConnection, ^(xpc_object_t object) {
|
||||
xpc_type_t type = xpc_get_type(object);
|
||||
if (type == XPC_TYPE_ERROR) {
|
||||
char *description = xpc_copy_description(object);
|
||||
NSLog(@"XPC peer error: %s", description);
|
||||
free(description);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type != XPC_TYPE_DICTIONARY) {
|
||||
NSLog(@"XPC peer expected dictionary");
|
||||
return;
|
||||
}
|
||||
|
||||
handleRequest(applicationIdentifier, object);
|
||||
});
|
||||
xpc_connection_resume(newConnection);
|
||||
}
|
||||
|
||||
// Listens for XPC messages from clients.
|
||||
//
|
||||
// Arguments are expected in the following order:
|
||||
//
|
||||
// jobLabel - The launchd job label for this task.
|
||||
//
|
||||
// Returns 0 on successful termination, non 0 otherwise.
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
atexit_b(^{
|
||||
NSLog(@"ShipIt quitting");
|
||||
});
|
||||
|
||||
if (argc < 2) {
|
||||
NSLog(@"Missing launchd job label for ShipIt");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
char const *jobLabel = argv[1];
|
||||
NSString *applicationIdentifier = @(jobLabel);
|
||||
|
||||
xpc_connection_t server = xpc_connection_create_mach_service(jobLabel, NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
|
||||
xpc_connection_set_event_handler(server, ^(xpc_object_t object) {
|
||||
xpc_type_t type = xpc_get_type(object);
|
||||
if (type == XPC_TYPE_ERROR) {
|
||||
char *description = xpc_copy_description(object);
|
||||
NSLog(@"XPC listener error: %s", description);
|
||||
free(description);
|
||||
} else if (type == XPC_TYPE_CONNECTION) {
|
||||
handleConnection(applicationIdentifier, object);
|
||||
}
|
||||
});
|
||||
xpc_connection_resume(server);
|
||||
|
||||
dispatch_main();
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
@@ -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());
|
||||
expect(signature).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
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
|
||||
|
||||
@@ -6,14 +6,7 @@
|
||||
// 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"
|
||||
|
||||
QuickSpecBegin(SQRLDeepCodesign)
|
||||
SpecBegin(SQRLDeepCodesign)
|
||||
|
||||
NSMutableDictionary * (^environmentSuitableForChildProcess)(void) = ^ {
|
||||
// Remove environment variables that configure the Obj-C runtime
|
||||
@@ -49,7 +42,7 @@ void (^resignTestApplicationPreserveEverythingButTheRequirements)(void) = ^{
|
||||
[resignCodesignTask launch];
|
||||
[resignCodesignTask waitUntilExit];
|
||||
|
||||
expect(@(resignCodesignTask.terminationStatus)).to(equal(@0));
|
||||
expect(resignCodesignTask.terminationStatus).to.equal(0);
|
||||
};
|
||||
|
||||
void (^deepCodesignTestApplication)(void) = ^{
|
||||
@@ -61,9 +54,9 @@ void (^deepCodesignTestApplication)(void) = ^{
|
||||
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());
|
||||
expect(getExecutable).to.beTruthy();
|
||||
expect(executable.boolValue).to.beTruthy();
|
||||
expect(executableError).to.beNil();
|
||||
|
||||
NSTask *deepCodesignTask = [[NSTask alloc] init];
|
||||
deepCodesignTask.launchPath = deepCodesignLocation.path;
|
||||
@@ -82,7 +75,7 @@ void (^deepCodesignTestApplication)(void) = ^{
|
||||
[deepCodesignTask launch];
|
||||
[deepCodesignTask waitUntilExit];
|
||||
|
||||
expect(@(deepCodesignTask.terminationStatus)).to(equal(@0));
|
||||
expect(deepCodesignTask.terminationStatus).to.equal(0);
|
||||
|
||||
/*
|
||||
By signing test application's contents, which are covered by test
|
||||
@@ -118,9 +111,9 @@ it(@"should deep sign the test application", ^{
|
||||
});
|
||||
|
||||
xit(@"should deep verify after signing", ^{
|
||||
expect(@(deepVerify())).to(beFalsy());
|
||||
expect(deepVerify()).to.beFalsy();
|
||||
deepCodesignTestApplication();
|
||||
expect(@(deepVerify())).to(beTruthy());
|
||||
expect(deepVerify()).to.beTruthy();
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -6,31 +6,26 @@
|
||||
// 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", ^{
|
||||
@@ -38,12 +33,12 @@ it(@"should send an Application Support URL", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
NSURL *appSupportURL = [[manager applicationSupportURL] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(appSupportURL).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
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
|
||||
|
||||
@@ -6,29 +6,24 @@
|
||||
// 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.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
#import "SQRLShipItConnection.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
|
||||
QuickSpecBegin(SQRLInstallerSpec)
|
||||
SpecBegin(SQRLInstaller)
|
||||
|
||||
mode_t (^modeOfURL)(NSURL *) = ^ mode_t (NSURL *fileURL) {
|
||||
NSFileSecurity *fileSecurity = nil;
|
||||
BOOL success = [fileURL getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:NULL];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(fileSecurity).notTo(beNil());
|
||||
expect(success).to.beTruthy();
|
||||
expect(fileSecurity).notTo.beNil();
|
||||
|
||||
__block mode_t mode;
|
||||
expect(@(CFFileSecurityGetMode((__bridge CFFileSecurityRef)fileSecurity, &mode))).to(beTruthy());
|
||||
expect(CFFileSecurityGetMode((__bridge CFFileSecurityRef)fileSecurity, &mode)).to.beTruthy();
|
||||
|
||||
return mode & (S_IRWXU | S_IRWXG | S_IRWXO);
|
||||
};
|
||||
@@ -40,71 +35,75 @@ beforeEach(^{
|
||||
});
|
||||
|
||||
it(@"should install an update using ShipIt", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should install an update in process", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation: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];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
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];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
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];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
NSURL *plistURL = [targetURL URLByAppendingPathComponent:@"Contents/Info.plist"];
|
||||
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"with backup restoration", ^{
|
||||
__block NSURL *targetURL;
|
||||
|
||||
__block SQRLShipItRequest *request;
|
||||
|
||||
beforeEach(^{
|
||||
targetURL = self.testApplicationURL;
|
||||
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
NSURL *copiedTargetURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication Target.app"];
|
||||
expect(@([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:copiedTargetURL error:NULL])).to(beTruthy());
|
||||
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());
|
||||
expect(ownedBundleArchive).notTo.beNil();
|
||||
|
||||
// Set up ShipIt's preferences like it paused in the middle of an
|
||||
// installation.
|
||||
@@ -114,71 +113,56 @@ describe(@"with backup restoration", ^{
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFDataRef)ownedBundleArchive, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
|
||||
BOOL synchronized = CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
expect(@(synchronized)).to(beTruthy());
|
||||
expect(synchronized).to.beTruthy();
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
__block NSError *error;
|
||||
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should not install an update after too many attempts", ^{
|
||||
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];
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES];
|
||||
|
||||
[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));
|
||||
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
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));
|
||||
expect(system(command.UTF8String)).to.equal(0);
|
||||
|
||||
expect(@(modeOfURL(updateURL))).to(equal(@0777));
|
||||
expect(@(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0777));
|
||||
expect(modeOfURL(updateURL)).to.equal(0777);
|
||||
expect(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0777);
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
|
||||
expect(@(modeOfURL(self.testApplicationURL))).to(equal(@0755));
|
||||
expect(@(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0755));
|
||||
expect(modeOfURL(self.testApplicationURL)).to.equal(0755);
|
||||
expect(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0755);
|
||||
});
|
||||
|
||||
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(5).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];
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
@@ -188,24 +172,32 @@ 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;
|
||||
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();
|
||||
system("killall -HUP shipit-installer");
|
||||
});
|
||||
|
||||
it(@"should handle SIGTERM", ^{
|
||||
system("killall -TERM ShipIt");
|
||||
verifyUpdate();
|
||||
system("killall -TERM shipit-installer");
|
||||
});
|
||||
|
||||
it(@"should handle SIGINT", ^{
|
||||
system("killall -INT ShipIt");
|
||||
verifyUpdate();
|
||||
system("killall -INT shipit-installer");
|
||||
});
|
||||
|
||||
it(@"should handle SIGQUIT", ^{
|
||||
system("killall -QUIT ShipIt");
|
||||
verifyUpdate();
|
||||
system("killall -QUIT shipit-installer");
|
||||
});
|
||||
|
||||
it(@"should handle SIGKILL", ^{
|
||||
@@ -214,16 +206,14 @@ describe(@"signal handling", ^{
|
||||
//
|
||||
// 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++) {
|
||||
system("killall -KILL ShipIt");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
system("killall -KILL shipit-installer");
|
||||
|
||||
// Wait at least for the launchd throttle interval.
|
||||
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
|
||||
[NSThread sleepForTimeInterval:delay];
|
||||
}
|
||||
|
||||
verifyUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -6,17 +6,10 @@
|
||||
// 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)
|
||||
SpecBegin(SQRLShipItRequest)
|
||||
|
||||
__block SQRLDirectoryManager *directoryManager;
|
||||
__block SQRLShipItRequest *request;
|
||||
@@ -25,46 +18,46 @@ 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());
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
|
||||
expect(request).notTo.beNil();
|
||||
|
||||
expect(request.targetBundleURL).to(equal(self.testApplicationURL));
|
||||
expect(request.updateBundleURL).to(equal(updateURL));
|
||||
expect(request.bundleIdentifier).to(beNil());
|
||||
expect(@(request.launchAfterInstallation)).to(beFalsy());
|
||||
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());
|
||||
|
||||
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));
|
||||
expect(requestCopy).to.equal(request);
|
||||
expect(requestCopy).notTo.beIdenticalTo(request);
|
||||
});
|
||||
|
||||
it(@"should fail to read state when no file exists yet", ^{
|
||||
it(@"should fail to read when no file exists yet", ^{
|
||||
NSError *error;
|
||||
BOOL success = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
BOOL success = [[SQRLShipItRequest readFromURL:[directoryManager.shipItStateURL first]] waitUntilCompleted:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(error).notTo.beNil();
|
||||
});
|
||||
|
||||
it(@"should write and read state to disk", ^{
|
||||
it(@"should write and read to disk", ^{
|
||||
NSError *error;
|
||||
BOOL success = [[request writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
BOOL success = [[request writeToURL:[directoryManager.shipItStateURL first]] 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());
|
||||
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readFromURL:[directoryManager.shipItStateURL first]] firstOrDefault:nil success:&success error:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(readRequest).to(equal(request));
|
||||
expect(readRequest).to.equal(request);
|
||||
});
|
||||
|
||||
it(@"should fail gracefully with archives encoding a different class", ^{
|
||||
@@ -72,15 +65,15 @@ it(@"should fail gracefully with archives encoding a different class", ^{
|
||||
|
||||
NSError *error;
|
||||
BOOL write = [[NSKeyedArchiver archivedDataWithRootObject:@"rogue object"] writeToURL:archiveLocation atomically:YES];
|
||||
expect(@(write)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
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)));
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest readFromURL:archiveLocation] firstOrDefault:nil success:&success error:&error];
|
||||
expect(request).to.beNil();
|
||||
expect(success).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLShipItRequestErrorDomain);
|
||||
expect(error.code).to.equal(SQRLShipItRequestErrorUnarchiving);
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
//
|
||||
// SQRLTerminationListenerSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-07.
|
||||
// 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)
|
||||
|
||||
__block SQRLTerminationListener *listener;
|
||||
|
||||
beforeEach(^{
|
||||
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
|
||||
expect(listener).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should complete immediately when the app is not running", ^{
|
||||
__block BOOL completed = NO;
|
||||
[[listener waitForTermination] subscribeCompleted:^{
|
||||
completed = YES;
|
||||
}];
|
||||
|
||||
expect(@(completed)).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should wait until one instance terminates", ^{
|
||||
NSRunningApplication *app = [self launchTestApplicationWithEnvironment:nil];
|
||||
|
||||
__block NSRunningApplication *observedApp = nil;
|
||||
__block BOOL completed = NO;
|
||||
[[listener waitForTermination] subscribeNext:^(id x) {
|
||||
observedApp = x;
|
||||
} completed:^{
|
||||
completed = YES;
|
||||
}];
|
||||
|
||||
expect(observedApp).toEventually(equal(app));
|
||||
expect(@(completed)).to(beFalsy());
|
||||
|
||||
[app forceTerminate];
|
||||
expect(@(app.terminated)).toEventually(beTruthy());
|
||||
expect(@(completed)).toEventually(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should wait until multiple instances terminate", ^{
|
||||
NSRunningApplication *app1 = [self launchTestApplicationWithEnvironment:nil];
|
||||
NSRunningApplication *app2 = [self launchTestApplicationWithEnvironment:nil];
|
||||
|
||||
__block BOOL completed = NO;
|
||||
[[listener waitForTermination] subscribeCompleted:^{
|
||||
completed = YES;
|
||||
}];
|
||||
|
||||
expect(@(completed)).to(beFalsy());
|
||||
|
||||
[app1 forceTerminate];
|
||||
expect(@(app1.terminated)).toEventually(beTruthy());
|
||||
expect(@(completed)).to(beFalsy());
|
||||
|
||||
[app2 forceTerminate];
|
||||
expect(@(app2.terminated)).toEventually(beTruthy());
|
||||
expect(@(completed)).toEventually(beTruthy());
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
@@ -1,16 +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>
|
||||
|
||||
// The short version string for the `testApplicationBundle`.
|
||||
extern NSString * const SQRLTestApplicationOriginalShortVersionString;
|
||||
|
||||
@@ -25,7 +20,7 @@ extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
@class SQRLDirectoryManager;
|
||||
@class SQRLShipItRequest;
|
||||
|
||||
@interface QuickSpec (SQRLFixtures)
|
||||
@interface SQRLTestCase : SPTXCTestCase
|
||||
|
||||
// A URL to a temporary directory tests can use.
|
||||
//
|
||||
@@ -77,6 +72,8 @@ extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
- (NSURL *)createTestApplicationUpdate;
|
||||
|
||||
// Runs the installer in process or submits ShipIt's launchd job to start it up.
|
||||
//
|
||||
// request - The install to send to ShipIt.
|
||||
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
|
||||
|
||||
// Creates a disk image, then mounts it.
|
||||
@@ -1,20 +1,20 @@
|
||||
//
|
||||
// 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 "SQRLShipItConnection.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLTestHelper.h"
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-retain-cycles"
|
||||
@@ -43,9 +43,90 @@ static void SQRLSignalHandler(int sig) {
|
||||
abort();
|
||||
}
|
||||
|
||||
@interface SQRLTestCase ()
|
||||
|
||||
// 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.
|
||||
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
|
||||
|
||||
// Returns an _unlaunched_ task that will follow log files at the given paths,
|
||||
// then pipe that output through this process.
|
||||
static NSTask *SQRLTailTaskWithPaths(RACSequence *paths) {
|
||||
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths;
|
||||
|
||||
@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",
|
||||
@"otest.ShipIt",
|
||||
@"otest-x86_64.ShipIt",
|
||||
];
|
||||
|
||||
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
|
||||
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
|
||||
return @[
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
|
||||
].rac_sequence;
|
||||
}];
|
||||
|
||||
for (NSURL *URL in URLs) {
|
||||
[[NSData data] writeToURL:URL atomically:YES];
|
||||
}
|
||||
|
||||
RACSequence *paths = [URLs map:^(NSURL *URL) {
|
||||
return URL.path;
|
||||
}];
|
||||
|
||||
NSTask *readShipIt = [self tailTaskWithPaths:paths];
|
||||
[readShipIt launch];
|
||||
|
||||
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
|
||||
|
||||
atexit_b(^{
|
||||
[readShipIt terminate];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
signal(SIGILL, &SQRLSignalHandler);
|
||||
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
|
||||
|
||||
Expecta.asynchronousTestTimeout = 3;
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[super tearDown];
|
||||
|
||||
SQRLKillAllTestApplications();
|
||||
}
|
||||
|
||||
- (void)addCleanupBlock:(dispatch_block_t)block {
|
||||
[SQRLTestHelper addCleanupBlock:block];
|
||||
}
|
||||
|
||||
#pragma mark Logging
|
||||
|
||||
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths {
|
||||
NSPipe *outputPipe = [NSPipe pipe];
|
||||
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
|
||||
|
||||
@@ -61,112 +142,24 @@ static NSTask *SQRLTailTaskWithPaths(RACSequence *paths) {
|
||||
return task;
|
||||
}
|
||||
|
||||
static NSBundle *SQRLTestBundle(void) {
|
||||
return [NSBundle bundleWithIdentifier:@"com.github.SquirrelTests"];
|
||||
}
|
||||
|
||||
static NSMutableArray *cleanupBlocks = nil;
|
||||
|
||||
// The URL to the temporary directory which contains `temporaryDirectoryURL` and
|
||||
// all copied test data.
|
||||
static NSURL *baseTemporaryDirectoryURL = nil;
|
||||
|
||||
QuickConfigurationBegin(Fixtures)
|
||||
|
||||
+ (void)configure:(Configuration *)configuration {
|
||||
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",
|
||||
];
|
||||
|
||||
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
|
||||
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
|
||||
return @[
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
|
||||
].rac_sequence;
|
||||
}];
|
||||
|
||||
RACSequence *paths = [URLs map:^(NSURL *URL) {
|
||||
return URL.path;
|
||||
}];
|
||||
|
||||
NSTask *readShipIt = SQRLTailTaskWithPaths(paths);
|
||||
|
||||
[configuration beforeSuite:^{
|
||||
signal(SIGILL, &SQRLSignalHandler);
|
||||
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
[readShipIt launch];
|
||||
|
||||
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
|
||||
}];
|
||||
|
||||
[configuration afterSuite:^{
|
||||
[readShipIt terminate];
|
||||
}];
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
[cleanupBlocks removeAllObjects];
|
||||
};
|
||||
|
||||
[configuration beforeEach:^{
|
||||
runCleanupBlocks();
|
||||
}];
|
||||
|
||||
[configuration afterEach:^{
|
||||
runCleanupBlocks();
|
||||
SQRLKillAllTestApplications();
|
||||
}];
|
||||
}
|
||||
|
||||
QuickConfigurationEnd
|
||||
|
||||
@implementation QuickSpec (SQRLFixtures)
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (void)addCleanupBlock:(dispatch_block_t)block {
|
||||
[cleanupBlocks addObject:[block copy]];
|
||||
}
|
||||
|
||||
#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];
|
||||
XCTAssertTrue(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 {
|
||||
@@ -184,7 +177,7 @@ 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"];
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
@@ -194,7 +187,7 @@ QuickConfigurationEnd
|
||||
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
|
||||
[[NSData data] writeToURL:testAppLog atomically:YES];
|
||||
|
||||
NSTask *readTestApp = SQRLTailTaskWithPaths([RACSequence return:testAppLog.path]);
|
||||
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSequence return:testAppLog.path]];
|
||||
[readTestApp launch];
|
||||
|
||||
XCTAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
|
||||
@@ -250,7 +243,7 @@ QuickConfigurationEnd
|
||||
}
|
||||
|
||||
- (NSURL *)createTestApplicationUpdate {
|
||||
NSURL *originalURL = [SQRLTestBundle() URLForResource:@"TestApplication 2.1" withExtension:@"app"];
|
||||
NSURL *originalURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication 2.1" withExtension:@"app"];
|
||||
XCTAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
@@ -269,7 +262,7 @@ QuickConfigurationEnd
|
||||
}
|
||||
|
||||
- (id)performWithTestApplicationRequirement:(id (^)(SecRequirementRef requirement))block {
|
||||
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
SecStaticCodeRef staticCode = NULL;
|
||||
@@ -292,7 +285,7 @@ QuickConfigurationEnd
|
||||
}
|
||||
|
||||
- (SQRLCodeSignature *)testApplicationSignature {
|
||||
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
@@ -303,7 +296,7 @@ QuickConfigurationEnd
|
||||
}
|
||||
|
||||
- (SQRLDirectoryManager *)shipItDirectoryManager {
|
||||
NSString *identifier = SQRLShipItLauncher.shipItJobLabel;
|
||||
NSString *identifier = SQRLShipItConnection.shipItJobLabel;
|
||||
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:identifier];
|
||||
XCTAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
|
||||
|
||||
@@ -312,31 +305,31 @@ QuickConfigurationEnd
|
||||
|
||||
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote {
|
||||
if (remote) {
|
||||
expect(@([[request writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL])).to(beTruthy());
|
||||
SQRLShipItConnection *connection = [[SQRLShipItConnection alloc] initWithRootPrivileges:NO];
|
||||
|
||||
__block NSError *error = nil;
|
||||
expect(@([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error])).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
expect([[connection sendRequest:request] waitUntilCompleted:&error]).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
// Remove ShipIt's launchd job so it doesn't relaunch itself.
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItConnection.shipItJobLabel, NULL, true, NULL);
|
||||
|
||||
NSError *lookupError;
|
||||
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
|
||||
expect(stateURL).notTo(beNil());
|
||||
expect(lookupError).to(beNil());
|
||||
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());
|
||||
expect(installer).notTo.beNil();
|
||||
|
||||
NSError *installedError = nil;
|
||||
BOOL installed = [[installer.installUpdateCommand execute:request] asynchronouslyWaitUntilCompleted:&installedError];
|
||||
expect(@(installed)).to(beTruthy());
|
||||
expect(installedError).to(beNil());
|
||||
expect(installed).to.beTruthy();
|
||||
expect(installedError).to.beNil();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,15 +344,15 @@ 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];
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// SQRLTestHelper.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Matt Diephouse on 7/7/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// A test helper class that will run cleanup blocks after each test finishes.
|
||||
@interface SQRLTestHelper : NSObject
|
||||
|
||||
/// Adds a cleanup block that will run after the next test completes.
|
||||
+ (void)addCleanupBlock:(dispatch_block_t)block;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// SQRLTestHelper.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Matt Diephouse on 7/7/14.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLTestHelper.h"
|
||||
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation SQRLTestHelper
|
||||
|
||||
#pragma mark - Specta Methods
|
||||
|
||||
+ (void)afterEach {
|
||||
// Enumerate backwards, so later resources are cleaned up first.
|
||||
for (dispatch_block_t block in self.cleanupBlocks.reverseObjectEnumerator) {
|
||||
block();
|
||||
}
|
||||
[self.cleanupBlocks removeAllObjects];
|
||||
}
|
||||
|
||||
#pragma mark - Public Methods
|
||||
|
||||
+ (void)addCleanupBlock:(dispatch_block_t)block {
|
||||
[self.cleanupBlocks addObject:[block copy]];
|
||||
}
|
||||
|
||||
#pragma mark - Private Methods
|
||||
|
||||
+ (NSMutableArray *)cleanupBlocks {
|
||||
NSMutableArray *blocks = objc_getAssociatedObject(self, "sqrl_cleanupBlocks");
|
||||
if (blocks == nil) {
|
||||
blocks = [NSMutableArray array];
|
||||
objc_setAssociatedObject(self, "sqrl_cleanupBlocks", blocks, OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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).notTo(beNil());
|
||||
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).notTo(beNil());
|
||||
expect(update.releaseDate).notTo.beNil();
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -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 "SQRLDirectoryManager.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
|
||||
#import "OHHTTPStubs/OHHTTPStubs.h"
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import "OHHTTPStubs/OHHTTPStubs.h"
|
||||
#import "TestAppConstants.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,7 +55,7 @@ beforeEach(^{
|
||||
|
||||
describe(@"updating", ^{
|
||||
__block NSURL *updateURL;
|
||||
|
||||
|
||||
beforeEach(^{
|
||||
updateURL = [self createTestApplicationUpdate];
|
||||
});
|
||||
@@ -72,19 +66,19 @@ describe(@"updating", ^{
|
||||
@"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)).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", ^{
|
||||
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),
|
||||
@@ -94,11 +88,11 @@ describe(@"updating", ^{
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(@(app.terminated)).withTimeout(5).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", ^{
|
||||
@@ -125,8 +119,8 @@ describe(@"updating", ^{
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
expect(@(app.terminated)).withTimeout(5).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", ^{
|
||||
@@ -137,22 +131,24 @@ describe(@"updating", ^{
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSTimeInterval delay = 15;
|
||||
NSTimeInterval delay = 30;
|
||||
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
|
||||
|
||||
expect(@(app.terminated)).withTimeout(delay + 5).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 applicationSupportURL] first];
|
||||
expect(appSupportURL).notTo(beNil());
|
||||
expect(appSupportURL).notTo.beNil();
|
||||
|
||||
updateDirectoryURLs = [[RACSignal
|
||||
defer:^{
|
||||
@@ -173,17 +169,16 @@ describe(@"updating", ^{
|
||||
@"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(5).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(@[]));
|
||||
expect([updateDirectoryURLs toArray]).will.equal(@[]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -192,7 +187,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];
|
||||
});
|
||||
@@ -209,9 +204,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", ^{
|
||||
@@ -226,9 +221,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(@(SQRLUpdaterErrorInvalidServerBody)));
|
||||
expect(result).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
|
||||
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -255,9 +250,9 @@ describe(@"state", ^{
|
||||
@(SQRLUpdaterStateCheckingForUpdate),
|
||||
@(SQRLUpdaterStateIdle),
|
||||
];
|
||||
expect(states).toEventually(equal(expectedStates));
|
||||
expect(states).will.equal(expectedStates);
|
||||
|
||||
expect(@(testApplication.terminated)).withTimeout(5).toEventually(beTruthy());
|
||||
expect(testApplication.terminated).will.beTruthy();
|
||||
});
|
||||
|
||||
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
|
||||
@@ -271,8 +266,8 @@ describe(@"state", ^{
|
||||
@"updateURL": zipUpdate([self createTestApplicationUpdate]),
|
||||
@"final": @YES,
|
||||
} error:&error];
|
||||
expect(update).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
expect(update).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
@@ -284,10 +279,10 @@ describe(@"state", ^{
|
||||
@(SQRLUpdaterStateDownloadingUpdate),
|
||||
@(SQRLUpdaterStateAwaitingRelaunch),
|
||||
];
|
||||
expect(states).toEventually(equal(expectedStates));
|
||||
expect(states).will.equal(expectedStates);
|
||||
|
||||
expect(@(testApplication.terminated)).withTimeout(5).toEventually(beTruthy());
|
||||
expect(testApplication.terminated).will.beTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// 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 <XCTest/XCTest.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"
|
||||
|
||||
// Expecta #defines startWith as beginWith, which messes up -startWith: :-(
|
||||
#undef startWith
|
||||
|
||||
#import "SQRLTestCase.h"
|
||||
#endif
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
#import "TestAppDelegate.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItConnection.h"
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
@@ -35,7 +35,7 @@
|
||||
NSLog(@"TestApplication quitting");
|
||||
});
|
||||
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL removed = [[[directoryManager
|
||||
@@ -72,8 +72,6 @@
|
||||
|
||||
__block NSUInteger updateCheckCount = 1;
|
||||
|
||||
NSInteger updateRequestCount = [NSProcessInfo.processInfo.environment[@"SQRLUpdateRequestCount"] integerValue];
|
||||
|
||||
[[[[[[[[[[RACSignal
|
||||
defer:^{
|
||||
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
|
||||
@@ -92,7 +90,7 @@
|
||||
|
||||
return testUpdate.final;
|
||||
}]
|
||||
take:updateRequestCount]
|
||||
take:1]
|
||||
doNext:^(id _) {
|
||||
NSLog(@"***** READY TO INSTALL UPDATE *****");
|
||||
}]
|
||||
|
||||
+2
-9
@@ -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..."
|
||||
|
||||
+7
-18
@@ -44,7 +44,7 @@ config ()
|
||||
# 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
|
||||
@@ -140,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
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ module Targets
|
||||
|
||||
# Look for an __TEXT,__info_plist section
|
||||
# This contains the CFBundleIdentifier necessary for signing
|
||||
`otool -l "#{path}"` =~ /__info_plist/
|
||||
`otool -l #{path}` =~ /__info_plist/
|
||||
end
|
||||
end
|
||||
|
||||
@@ -106,10 +106,8 @@ end
|
||||
|
||||
module Codesign
|
||||
class Task
|
||||
def self.sign(target, identity, timestamp)
|
||||
arguments = [ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,requirements", "--verbose", target.path ]
|
||||
arguments.unshift("--timestamp=none") if not timestamp
|
||||
codesign_with_arguments(arguments)
|
||||
def self.sign(target, identity)
|
||||
codesign_with_arguments([ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,resource-rules,requirements", "--verbose", target.path ])
|
||||
end
|
||||
|
||||
protected
|
||||
@@ -139,7 +137,7 @@ unless build_dir && product_name
|
||||
exit_error("Path to built product not provided")
|
||||
end
|
||||
|
||||
input_path = File.realpath(File.join(build_dir, product_name))
|
||||
input_path = File.join(build_dir, product_name)
|
||||
unless File.exists?(input_path)
|
||||
exit_error("Built product doesn't exist at path provided")
|
||||
end
|
||||
@@ -170,7 +168,6 @@ while paths_to_search.size > 0
|
||||
|
||||
Dir.foreach(search_path) do |current_directory_entry|
|
||||
next if current_directory_entry.start_with?(".")
|
||||
File.delete(File.join(search_path, current_directory_entry)) if current_directory_entry.end_with?(".cstemp")
|
||||
|
||||
target = match_path(File.join(search_path, current_directory_entry))
|
||||
next if target.nil?
|
||||
@@ -196,8 +193,7 @@ targets = targets.sort_by do |target|
|
||||
end
|
||||
targets.reverse!
|
||||
|
||||
use_timestamp = ENV["CONFIGURATION"] == "Release"
|
||||
targets.each do |target|
|
||||
puts "=> #{target.path}"
|
||||
Codesign::Task.sign(target, codesign_identity, use_timestamp)
|
||||
Codesign::Task.sign(target, codesign_identity)
|
||||
end
|
||||
|
||||
+3
-1
@@ -2,8 +2,10 @@ BEGIN {
|
||||
FS = "\n";
|
||||
}
|
||||
|
||||
/Schemes:/ {
|
||||
/Targets:/ {
|
||||
while (getline && $0 != "") {
|
||||
if ($0 ~ /Test/) continue;
|
||||
|
||||
sub(/^ +/, "");
|
||||
print "'" $0 "'";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user