Author SHA1 Message Date
Keith Duncan 0828897f1b 🍩 pass the request path around 2014-09-02 17:08:50 +02:00
Keith Duncan ff31a5fcdd Replace the installer with an XPC version
Doesn’t read install requests from the file system, also makes the installer installable once rather than once per install request.
2014-09-01 22:28:31 +02:00
Keith Duncan 016cd25fda Update docs 2014-09-01 22:23:13 +02:00
Keith Duncan afebefc78a Remove the watcher 2014-08-05 21:46:50 +01:00
Keith Duncan 3c7916e02e More building++ 2014-08-05 21:20:47 +01:00
Keith Duncan 8d6063d9a9 Fix missed conflict in merge 2014-08-05 21:17:50 +01:00
Keith Duncan 24c118fa1e xctest? 2014-08-05 21:17:48 +01:00
Keith Duncan ff7b9de34f Remove the watcher 2014-08-05 21:17:19 +01:00
Keith Duncan 5994cf9584 Merge remote-tracking branch 'origin/master' into dont-link-appkit
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLShipItRequest.m
	SquirrelTests/SQRLTestCase.m
2014-08-05 21:05:49 +01:00
Keith Duncan 518ee666a3 Restrict the permissions of the wait files directory 2014-02-19 14:46:17 +00:00
Keith Duncan 73cc6802d3 Clarify the installer args 2014-02-19 13:58:26 +00:00
Keith Duncan 1403a7ad81 Make the watcher job dictionary method return a signal
Fix the signal names for the job dictionary methods.
Extract the watcher job label into a method.
2014-02-19 13:57:16 +00:00
Keith Duncan 24d9ef1f86 Remove the debug launchd option 2014-02-18 13:51:52 +00:00
Keith Duncan 51bd593666 Remove the domain/authorisation tuple 2014-02-18 13:12:27 +00:00
Keith Duncan 44a471b903 Concat the submit job signals inline 2014-02-18 13:12:13 +00:00
Keith Duncan 6e2f4faa3f Style++ 2014-02-18 13:06:33 +00:00
Keith Duncan 43fd18e4c9 Less URL->path conversions 2014-02-18 13:06:02 +00:00
Keith Duncan d08804ddce Less URL->path conversions 2014-02-18 13:01:04 +00:00
Keith Duncan 39a47ac14f s/command/executableName 2014-02-18 12:58:35 +00:00
Keith Duncan 49148013dc Request cannot be nil 2014-02-18 12:57:51 +00:00
Keith Duncan 15c80b1cbf Better docs for the send request method 2014-02-18 12:57:14 +00:00
Keith Duncan f89762550f Rename the connection initialiser 2014-02-18 12:54:59 +00:00
Keith Duncan b224b30a27 s/[[:space:]]/[[:tab:]] 2014-02-14 19:12:08 +00:00
Keith Duncan bfa660b5bc Add docs on the multiprocess topology to SQRLShipItConnection 2014-02-14 19:03:00 +00:00
Keith Duncan 3ed1dc26fa Add docs for the installer args 2014-02-12 16:34:23 +00:00
Keith Duncan 442e51b89d Change the watcher args to be URLs 2014-02-12 16:34:04 +00:00
Keith Duncan a8b6a9f262 Condense the job dictionary creation 2014-02-12 16:26:34 +00:00
Keith Duncan 05019c8e82 New file listener API
Now with 💯% more error checking
2014-02-12 16:13:45 +00:00
Keith Duncan d569a6524e Document the shipit-watcher arguments 2014-02-12 12:57:29 +00:00
Keith Duncan 5c64d501f4 Style++ 2014-02-12 12:43:25 +00:00
Keith Duncan ee8c781284 Fix signal names 2014-02-06 21:22:38 +00:00
Keith Duncan d8b1c4b6ca Merge remote-tracking branch 'origin/master' into dont-link-appkit
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLInstaller.m
	Squirrel/SQRLShipItState.h
	Squirrel/SQRLShipItState.m
	Squirrel/SQRLUpdater.m
	Squirrel/ShipIt-main.m
	SquirrelTests/SQRLInstallerSpec.m
	SquirrelTests/SQRLShipItStateSpec.m
	SquirrelTests/SQRLTestCase.h
	SquirrelTests/SQRLTestCase.m
2014-02-06 21:22:15 +00:00
Keith Duncan 3582cdec0b Fix the signal tests to use the new process name 2014-01-20 16:26:39 -08:00
Keith Duncan ef169d19e9 Fix task launching on 10.7 2014-01-20 16:19:19 -08:00
Keith Duncan 582593dca7 Fix 10.7 support 2014-01-20 16:14:24 -08:00
Keith Duncan ba6df2df14 Fix argument docs 2014-01-20 16:09:13 -08:00
Keith Duncan 65463d7612 Rename signal 2014-01-20 16:07:54 -08:00
Keith Duncan 1f38d749ea Use the job label when removing and adding 2014-01-20 16:00:14 -08:00
Keith Duncan c4871aa371 Add launch support using LaunchServices directly 2014-01-20 15:18:15 -08:00
Keith Duncan f067d6b778 Zip++ 2014-01-20 15:18:11 -08:00
Keith Duncan 2c10c116e9 Clean up the wait file when the installer completes 2014-01-20 12:53:05 -08:00
Keith Duncan cfc2d55ca9 Write the state before submitting the jobs 2014-01-20 12:40:22 -08:00
Keith Duncan daaf62645e Don’t pass the label to the watcher 2014-01-20 12:40:15 -08:00
Keith Duncan f34c02b79b Run watcher on launch 2014-01-20 12:29:23 -08:00
Keith Duncan 5d5f5eaa12 Make submitJob:... a cold signal 2014-01-20 12:05:01 -08:00
Keith Duncan 40e5ac130e Fix types issue, the authorization in the tuple is a signal 2014-01-20 11:57:18 -08:00
Keith Duncan e449f18bd3 Tweak test description 2014-01-20 11:56:54 -08:00
Keith Duncan 6d4f8a731b Xcode updating schemes 2014-01-17 17:20:16 -08:00
Keith Duncan 9debcdcd57 Update to the new NSURL argument for state read/write 2014-01-17 17:19:57 -08:00
Keith Duncan 4d489e5add Style++ 2014-01-17 17:19:37 -08:00
Keith Duncan 60f4089f6f Use the new file watcher rather than termination listener 2014-01-17 17:19:13 -08:00
Keith Duncan 33d2c31db9 Switch to using the NSURL argument type 2014-01-17 17:18:46 -08:00
Keith Duncan 267cd56bd6 Add file listener class
Parallels the termination listener
2014-01-17 17:13:22 -08:00
Keith Duncan 2a4147f3b1 Make ShipIt state take plain NSURL arguments
Get the watcher job to launch too, still needs to listen for its file in the installer.
2014-01-17 12:30:12 -08:00
Keith Duncan fb991f348b Rename job submit method 2014-01-17 11:37:04 -08:00
Keith Duncan 839f4a9708 Remove Mach service, this isn’t used 2014-01-17 11:31:37 -08:00
Keith Duncan e142e1bef0 Implement the new watcher process 2014-01-15 13:48:40 -08:00
Keith Duncan 14892783cd Start a new target for the watcher 2014-01-15 10:25:42 -08:00
Keith Duncan 88c65394c1 Implement a separate watch command
This is so that we can split the waiting into running in the user launchd domain, and the install still runs in the root domain.
2014-01-15 10:16:17 -08:00
Keith Duncan afc0ad0b20 Don’t use NSRunningApplication in SQRLDirectoryManager
This is AppKit API and `SQRLDirectoryManager` should be Foundation only.
2014-01-15 10:11:21 -08:00
Keith Duncan 0c533da4d7 Start launch logic in SQRLShipItConnection 2014-01-12 20:45:17 +00:00
Keith Duncan a5f8798ce8 Remove the Cocoa linkage from ShipIt, link Foundation instead 2014-01-12 20:20:50 +00:00
Keith Duncan ab52a12630 Remove launch handing from ShipIt-main
This will be handled external to ShipIt which can run as root and therefore should not fork or comminicate with LaunchServices.
2014-01-12 20:18:17 +00:00
Keith Duncan 954de5d480 Move SQRLTestCase to the top of the group 2014-01-12 20:17:31 +00:00
Keith Duncan df71c4d256 Renme the launchShipIt method 2014-01-12 20:17:15 +00:00
Keith Duncan d73df99d57 Remove un-necessary +defer 2014-01-12 20:16:54 +00:00
Keith Duncan bfc2652481 Pass the SQRLShipItState to SQRLShipItConnection for writing
The launched ShipIt is now a function of the written request.
2014-01-12 19:56:24 +00:00
Keith Duncan 90f31dd0f8 Use a connection instance to send the request
Whether to launch the target is now an argument, this needs to be handled by this process not the aux. process which may be root.
2014-01-12 19:41:20 +00:00
Keith Duncan 470338574f Rename SQRLShipItLauncher to SQRLShipItConnection 2014-01-12 19:34:08 +00:00
72 changed files with 1661 additions and 1924 deletions
+6 -19
View File
@@ -1,22 +1,9 @@
[submodule "External/ReactiveCocoa"]
path = External/ReactiveCocoa
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/MantleFramework/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
[submodule "Carthage.checkout/Nimble"]
path = Carthage/Checkouts/Nimble
url = https://github.com/Quick/Nimble.git
[submodule "Carthage.checkout/ReactiveCocoa"]
path = Carthage/Checkouts/ReactiveCocoa
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "Carthage.checkout/Quick"]
path = Carthage/Checkouts/Quick
url = https://github.com/Quick/Quick.git
[submodule "Carthage.checkout/xcconfigs"]
path = Carthage/Checkouts/xcconfigs
url = https://github.com/jspahrsummers/xcconfigs.git
[submodule "Carthage.checkout/Mantle"]
path = Carthage/Checkouts/Mantle
url = https://github.com/Mantle/Mantle.git
[submodule "Carthage/Checkouts/Nimble"]
url = https://github.com/Quick/Nimble.git
[submodule "Carthage/Checkouts/Quick"]
url = https://github.com/Quick/Quick.git
-12
View File
@@ -1,12 +0,0 @@
osx_image: xcode7.1
language: objective-c
before_install:
- brew update
- brew outdated xctool || brew upgrade xctool
install: script/bootstrap
script: script/cibuild
notifications:
email: false
env:
global:
secure: L4C3hHzXc/sgL35dIz3kjqIzPWnXCHsFOgbOcz0f249zlnegeKHQdChm4HUDtfrbmKLMYQCIvVfcgPeLaHkCSeHqXUihXLT2LglK+0GX3MBbFzde9x1uln2O68hH1tTmBg2O2i4Dtc4D3e+2tVv3XmhQDZt0znIVO6n9YgevDqw=
-2
View File
@@ -1,2 +0,0 @@
github "Mantle/Mantle" ~> 1.5.3
github "ReactiveCocoa/ReactiveCocoa" ~> 2.4.3
-3
View File
@@ -1,3 +0,0 @@
github "jspahrsummers/xcconfigs" >= 0.7.1
github "Quick/Quick" ~> 0.7
github "Quick/Nimble" ~> 2.0
-5
View File
@@ -1,5 +0,0 @@
github "Mantle/Mantle" "1.5.6"
github "Quick/Nimble" "v2.0.0"
github "Quick/Quick" "v0.7.0"
github "ReactiveCocoa/ReactiveCocoa" "v2.5"
github "jspahrsummers/xcconfigs" "0.8.1"
Vendored Submodule
+1
Submodule External/Mantle added at fe3a65987b
Vendored Submodule
+1
Submodule External/ReactiveCocoa added at 16c181bbe8
+8 -15
View File
@@ -6,7 +6,6 @@ and transparent as updates to a website**.
Instead of publishing a feed of versions from which your app must select,
Squirrel updates to the version your server tells it to. This allows you to
intelligently update your clients based on the request you give to Squirrel.
The server can remotely drive behaviors like rolling back or phased rollouts.
Your request can include authentication details, custom headers or a request
body so that your server has the context it needs in order to supply the most
@@ -40,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.
@@ -114,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) {
@@ -159,7 +152,7 @@ to the update request provided:
"url": "http://mycompany.com/myapp/releases/myrelease",
"name": "My Release Name",
"notes": "Theses are some release notes innit",
"pub_date": "2013-09-18T12:29:53+01:00"
"pub_date": "2013-09-18T12:29:53+01:00",
}
```
@@ -176,4 +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).
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0710"
LastUpgradeVersion = "0510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -15,50 +15,48 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
BuildableName = "shipit-installer"
BlueprintName = "ShipIt Installer"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
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>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0710"
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"
@@ -37,10 +37,10 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO">
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
<TestableReference
skipped = "NO">
@@ -53,45 +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>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQRLUpdateFromURL"
@@ -103,20 +74,11 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Profile"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0710"
LastUpgradeVersion = "0510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -23,10 +23,10 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
</Testables>
<MacroExpansion>
@@ -38,21 +38,17 @@
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -65,13 +61,12 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
+8 -8
View File
@@ -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.
+2 -5
View File
@@ -7,11 +7,8 @@
//
#import "SQRLCodeSignature.h"
#import "EXTKeyPathCoding.h"
#import "EXTScope.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
+3 -13
View File
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
@class RACSignal;
@@ -29,24 +29,14 @@
// on-disk locations for. This must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
// Finds or creates a storage folder.
// Finds or creates an Application Support folder.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)storageURL;
- (RACSignal *)applicationSupportURL;
// Determines where archived `SQRLShipItState` should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStateURL;
// Determines where ShipIt's stdout log should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStdoutURL;
// Determines where ShipIt's stderr log should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStderrURL;
@end
+6 -25
View File
@@ -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");
@@ -47,11 +46,11 @@
#pragma mark Folder URLs
- (RACSignal *)storageURL {
- (RACSignal *)applicationSupportURL {
return [[RACSignal
defer:^{
NSError *error = nil;
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (folderURL == nil) {
return [RACSignal error:error];
}
@@ -63,36 +62,18 @@
return [RACSignal return:folderURL];
}]
setNameWithFormat:@"%@ -storageURL", self];
setNameWithFormat:@"%@ -applicationSupportURL", self];
}
- (RACSignal *)shipItStateURL {
return [[[self
storageURL]
applicationSupportURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipItState.plist"];
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
}
- (RACSignal *)shipItStdoutURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stdout.log"];
}]
setNameWithFormat:@"%@ -shipItStdoutURL", self];
}
- (RACSignal *)shipItStderrURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stderr.log"];
}]
setNameWithFormat:@"%@ -shipItStderrURL", self];
}
#pragma mark NSObject
- (NSString *)description {
+2 -2
View File
@@ -40,9 +40,9 @@
#pragma mark MTLJSONSerializing
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
};
}];
}
@end
+22 -84
View File
@@ -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
+1 -1
View File
@@ -8,7 +8,7 @@
#import "SQRLInstallerOwnedBundle.h"
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/EXTKeyPathCoding.h>
@implementation SQRLInstallerOwnedBundle
+81
View File
@@ -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
+203
View File
@@ -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
-34
View File
@@ -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
-150
View File
@@ -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 [RACSignal zip:@[ [directoryManager shipItStdoutURL], [directoryManager shipItStderrURL] ]];
}]
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL) {
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)] = stdoutURL.path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = stderrURL.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
+21 -15
View File
@@ -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
+11 -16
View File
@@ -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),
-32
View File
@@ -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
-95
View File
@@ -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
+1 -1
View File
@@ -74,7 +74,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
+ (NSValueTransformer *)releaseDateJSONTransformer {
// ISO 8601 Time Zone with ':'
NSString * const ISO8601DateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZZZ";
NSString * const ISO8601DateFormat = @"yyyy'-'MM'-'DD'T'HH':'mm':'ssZZZZZ";
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^ NSDate * (NSString *dateString) {
if (![dateString isKindOfClass:NSString.class]) return nil;
-3
View File
@@ -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.
+81 -136
View File
@@ -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"
@@ -40,18 +41,22 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@property (atomic, readwrite) SQRLUpdaterState state;
/// The etag of the currently downloaded update, nil if no update has been
/// downloaded.
@property (atomic, copy) NSString *etag;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
// 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.
//
@@ -163,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);
@@ -171,8 +203,12 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[[[self
performHousekeeping]
// Prune old updates before the first update check.
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
then:^{
self.state = SQRLUpdaterStateCheckingForUpdate;
@@ -220,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;
}
@@ -328,20 +363,12 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSURL *zipDownloadURL = update.updateURL;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
if (self.etag != nil) {
[zipDownloadRequest setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
return [[[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (httpResponse.statusCode == 304 /* Not Modified */) {
return [RACSignal empty];
}
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
@@ -351,8 +378,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
self.etag = httpResponse.allHeaderFields[@"ETag"];
}
return [RACSignal return:bodyData];
@@ -393,11 +418,11 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -467,90 +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];
}
- (RACSignal *)performHousekeeping {
return [[RACSignal
merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
catch:^(NSError *error) {
NSLog(@"Error doing housekeeping: %@", error);
return [RACSignal empty];
}];
}
/// Lazily removes outdated temporary directories (used for previous updates)
/// upon subscription.
///
/// Pruning directories while an update is pending or in progress will result in
/// undefined behavior.
///
/// Sends each removed directory then completes, or errors, on an unspecified
/// thread.
- (RACSignal *)pruneUpdateDirectories {
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *storageURL) {
NSFileManager *manager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
return YES;
}];
return [[enumerator.rac_sequence.signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
}]
doNext:^(NSURL *directoryURL) {
NSError *error = nil;
if (![manager removeItemAtURL:directoryURL error:&error]) {
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
}
}];
}]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
}
// Like truncation, but backwards.
- (RACSignal *)backwardTruncateFile:(NSURL *)fileURL {
return [RACSignal defer:^{
static const NSInteger MAX_LENGTH = 1024 * 1024 * 8;
NSError *error;
NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:fileURL error:&error];
if (handle == nil) return [RACSignal error:error];
unsigned long long fileLength = [handle seekToEndOfFile];
if (fileLength <= MAX_LENGTH) return [RACSignal empty];
[handle seekToFileOffset:fileLength - MAX_LENGTH];
NSData *mostRecentData = [handle readDataToEndOfFile];
[handle truncateFileAtOffset:0];
[handle writeData:mostRecentData];
return [RACSignal empty];
}];
}
- (RACSignal *)truncateLogs {
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [RACSignal zip:@[ [directoryManager shipItStdoutURL], [directoryManager shipItStderrURL] ]];
}]
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL) {
return [RACSignal merge:@[ [self backwardTruncateFile:stdoutURL], [self backwardTruncateFile:stderrURL] ]];
}];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return directoryManager.shipItStateURL;
}
#pragma mark Installing Updates
@@ -579,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]
-165
View File
@@ -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:@{} 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;
}
+170
View File
@@ -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;
}
+1 -1
View File
@@ -9,7 +9,7 @@
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
+8
View File
@@ -0,0 +1,8 @@
//
// Prefix header for all source files of the 'Squirrel' target in the 'Squirrel' project
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SystemConfiguration/SystemConfiguration.h>
#endif
-8
View File
@@ -6,14 +6,6 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Squirrel.
FOUNDATION_EXPORT double SquirrelVersionNumber;
//! Project version string for Squirrel.
FOUNDATION_EXPORT const unsigned char SquirrelVersionString[];
#import <Squirrel/NSBundle+SQRLVersionExtensions.h>
#import <Squirrel/NSProcessInfo+SQRLVersionExtensions.h>
#import <Squirrel/SQRLDownloadedUpdate.h>
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
+7 -12
View File
@@ -6,35 +6,30 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(NSProcessInfoExtensions)
SpecBegin(NSProcessInfoExtensions)
describe(@"-sqrl_operatingSystemShortVersionString", ^{
__block NSString *versionString;
beforeEach(^{
versionString = NSProcessInfo.processInfo.sqrl_operatingSystemShortVersionString;
expect(versionString).notTo(beNil());
expect(versionString).notTo.beNil();
});
it(@"should follow major.minor.patch format", ^{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[0-9]+(\\.[0-9]+)*$" options:0 error:&error];
expect(regex).notTo(beNil());
expect(error).to(beNil());
expect(regex).notTo.beNil();
expect(error).to.beNil();
NSRange fullRange = NSMakeRange(0, versionString.length);
NSRange matchRange = [regex rangeOfFirstMatchInString:versionString options:0 range:fullRange];
expect(@(NSEqualRanges(matchRange, fullRange))).to(beTruthy());
expect(NSEqualRanges(matchRange, fullRange)).to.beTruthy();
});
it(@"should be numerically comparable", ^{
expect(@([@"10.6.9" compare:versionString options:NSNumericSearch])).to(equal(@(NSOrderedAscending)));
expect([@"10.6.9" compare:versionString options:NSNumericSearch]).to.equal(NSOrderedAscending);
});
});
QuickSpecEnd
SpecEnd
+56 -63
View File
@@ -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
+119
View File
@@ -0,0 +1,119 @@
//
// SQRLDeepCodesignSpec.m
// Squirrel
//
// Created by Keith Duncan on 2013-09-05.
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLDeepCodesign)
NSMutableDictionary * (^environmentSuitableForChildProcess)(void) = ^ {
// Remove environment variables that configure the Obj-C runtime
// Specifically OBJC_DISABLE_GC so that child processes aren't forced to
// adopt the GC preference of the test suite
NSMutableDictionary *environment = [NSProcessInfo.processInfo.environment mutableCopy];
NSSet *objcEnvironmentVariables = [environment keysOfEntriesPassingTest:^(NSString *variable, id obj, BOOL *stop) {
return [variable hasPrefix:@"OBJC"];
}];
[environment removeObjectsForKeys:objcEnvironmentVariables.allObjects];
return environment;
};
NSTask * (^codesignTaskWithArguments)(NSArray *) = ^ (NSArray *arguments) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/xcrun";
task.arguments = [@[ @"codesign" ] arrayByAddingObjectsFromArray:arguments];
task.environment = environmentSuitableForChildProcess();
return task;
};
void (^resignTestApplicationPreserveEverythingButTheRequirements)(void) = ^{
NSURL *testApplicationLocation = self.testApplicationURL;
NSTask *resignCodesignTask = codesignTaskWithArguments(@[
@"--sign", @"-",
@"--force",
@"--verbose=4",
@"--preserve-metadata=identifier,entitlements,resource-rules",
testApplicationLocation.path,
]);
[resignCodesignTask launch];
[resignCodesignTask waitUntilExit];
expect(resignCodesignTask.terminationStatus).to.equal(0);
};
void (^deepCodesignTestApplication)(void) = ^{
NSURL *testApplicationLocation = self.testApplicationURL;
NSBundle *testsBundle = [NSBundle bundleForClass:self.class];
NSURL *deepCodesignLocation = [testsBundle URLForResource:@"deep-codesign" withExtension:nil];
NSNumber *executable = nil;
NSError *executableError = nil;
BOOL getExecutable = [deepCodesignLocation getResourceValue:&executable forKey:NSURLIsExecutableKey error:&executableError];
expect(getExecutable).to.beTruthy();
expect(executable.boolValue).to.beTruthy();
expect(executableError).to.beNil();
NSTask *deepCodesignTask = [[NSTask alloc] init];
deepCodesignTask.launchPath = deepCodesignLocation.path;
deepCodesignTask.standardError = [NSPipe pipe];
deepCodesignTask.standardOutput = [NSPipe pipe];
NSMutableDictionary *environment = environmentSuitableForChildProcess();
[environment addEntriesFromDictionary:@{
@"CODE_SIGN_IDENTITY": @"-",
@"CONFIGURATION_BUILD_DIR": testApplicationLocation.URLByDeletingLastPathComponent.path,
@"FULL_PRODUCT_NAME": testApplicationLocation.lastPathComponent,
}];
deepCodesignTask.environment = environment;
[deepCodesignTask launch];
[deepCodesignTask waitUntilExit];
expect(deepCodesignTask.terminationStatus).to.equal(0);
/*
By signing test application's contents, which are covered by test
application's ResourceRules it's signature becomes invalid. Usually,
Xcode would sign the application after deep-codesign has run so the
signature would include the _signed_ child resources which are covered
by test application's ResourceRules
test application is already signed, so we need to manually resign it
deep-codesign preserves the requirements, we can't use it to resign the
root target, the new signing identity wouldn't verify against the
original designated requirement
*/
resignTestApplicationPreserveEverythingButTheRequirements();
};
BOOL (^deepVerify)(void) = ^ BOOL {
NSTask *deepVerifyTask = codesignTaskWithArguments(@[
@"--deep-verify",
@"--verbose=4",
self.testApplicationURL.path
]);
[deepVerifyTask launch];
[deepVerifyTask waitUntilExit];
return deepVerifyTask.terminationStatus == 0;
};
it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
xit(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
});
SpecEnd
+14 -19
View File
@@ -6,44 +6,39 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
QuickSpecBegin(SQRLDirectoryManagerSpec)
SpecBegin(SQRLDirectoryManager)
__block NSString *otestIdentifier;
beforeEach(^{
otestIdentifier = NSProcessInfo.processInfo.environment[@"FORCE_APP_IDENTIFIER"];
expect(otestIdentifier).notTo(beNil());
otestIdentifier = NSRunningApplication.currentApplication.localizedName;
expect(otestIdentifier).notTo.beNil();
});
it(@"should initialize", ^{
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier];
expect(manager).notTo(beNil());
expect(manager).notTo.beNil();
});
it(@"should create a manager for the current app", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
expect(manager).notTo(beNil());
expect(manager).to(equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]));
expect(manager).notTo.beNil();
expect(manager).to.equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]);
});
it(@"should send an Application Support URL", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
NSError *error = nil;
NSURL *appSupportURL = [[manager storageURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo(beNil());
expect(error).to(beNil());
NSURL *appSupportURL = [[manager applicationSupportURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo.beNil();
expect(error).to.beNil();
__block BOOL directory = NO;
expect(@([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory])).to(beTruthy());
expect(@(directory)).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory]).to.beTruthy();
expect(directory).to.beTruthy();
});
it(@"should send a ShipIt state URL", ^{
@@ -51,8 +46,8 @@ it(@"should send a ShipIt state URL", ^{
NSError *error = nil;
NSURL *stateURL = [[manager shipItStateURL] firstOrDefault:nil success:NULL error:&error];
expect(stateURL).notTo(beNil());
expect(error).to(beNil());
expect(stateURL).notTo.beNil();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+64 -74
View File
@@ -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]).withTimeout(SQRLLongTimeout).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(SQRLLongTimeout).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
NSError *error;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
};
beforeEach(^{
// Copied so that we don't recreate the TestApplication bundle by
// accessing the property.
targetURL = self.testApplicationURL;
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
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
+31 -38
View File
@@ -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,79 +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", ^{
SKIP_IF_RUNNING_ON_TRAVIS
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", ^{
SKIP_IF_RUNNING_ON_TRAVIS
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,19 +1,11 @@
//
// QuickSpec+SQRLFixtures.h
// SQRLTestCase.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
/// See https://github.com/Squirrel/Squirrel.Mac/pull/154#issuecomment-151287744
#define SKIP_IF_RUNNING_ON_TRAVIS if (self.runningOnTravis) return;
// The short version string for the `testApplicationBundle`.
extern NSString * const SQRLTestApplicationOriginalShortVersionString;
@@ -24,15 +16,11 @@ extern NSString * const SQRLTestApplicationUpdatedShortVersionString;
// The Info.plist key associated with a bundle's short version string.
extern NSString * const SQRLBundleShortVersionStringKey;
/// The longer timeout we should use for things that take a long time. This is
/// especially needed on slower machines (e.g., Travis CI).
extern const NSTimeInterval SQRLLongTimeout;
@class SQRLCodeSignature;
@class SQRLDirectoryManager;
@class SQRLShipItRequest;
@interface QuickSpec (SQRLFixtures)
@interface SQRLTestCase : SPTXCTestCase
// A URL to a temporary directory tests can use.
//
@@ -61,9 +49,6 @@ extern const NSTimeInterval SQRLLongTimeout;
// A directory manager for finding URLs that apply to ShipIt.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *shipItDirectoryManager;
/// Are the tests currently being run on Travis?
@property (nonatomic, readonly, assign, getter = isRunningOnTravis) BOOL runningOnTravis;
// Launches a new copy of TestApplication.app from the `testApplicationBundle`.
//
// Invoking this method multiple times will result in multiple running instances
@@ -87,6 +72,8 @@ extern const NSTimeInterval SQRLLongTimeout;
- (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"
@@ -23,8 +23,6 @@ NSString * const SQRLTestApplicationOriginalShortVersionString = @"1.0";
NSString * const SQRLTestApplicationUpdatedShortVersionString = @"2.1";
NSString * const SQRLBundleShortVersionStringKey = @"CFBundleShortVersionString";
const NSTimeInterval SQRLLongTimeout = 20;
static void SQRLKillAllTestApplications(void) {
// Forcibly kill all copies of the TestApplication that may be running.
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"];
@@ -45,25 +43,40 @@ static void SQRLSignalHandler(int sig) {
abort();
}
static NSBundle *SQRLTestBundle(void) {
return [NSBundle bundleWithIdentifier:@"com.github.SquirrelTests"];
}
@interface SQRLTestCase ()
static NSMutableArray *cleanupBlocks = nil;
// An array of dispatch_block_t values, for performing cleanup after the current
// example finishes.
//
// This array will be emptied between each example.
@property (nonatomic, strong, readonly) NSMutableArray *exampleCleanupBlocks;
// The URL to the temporary directory which contains `temporaryDirectoryURL` and
// all copied test data.
static NSURL *baseTemporaryDirectoryURL = nil;
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
QuickConfigurationBegin(Fixtures)
// Returns an _unlaunched_ task that will follow log files at the given paths,
// then pipe that output through this process.
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths;
+ (void)configure:(Configuration *)configuration {
@end
@implementation SQRLTestCase
#pragma mark Properties
@synthesize baseTemporaryDirectoryURL = _baseTemporaryDirectoryURL;
#pragma mark Lifecycle
+ (void)load {
NSURL *appSupportURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
NSAssert(appSupportURL != nil, @"Could not find Application Support folder");
NSArray *folders = @[
@"com.github.Squirrel.TestApplication.ShipIt",
@"com.github.Squirrel.SquirrelTests.ShipIt",
@"otest.ShipIt",
@"otest-x86_64.ShipIt",
];
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
@@ -74,69 +87,79 @@ QuickConfigurationBegin(Fixtures)
].rac_sequence;
}];
[configuration beforeSuite:^{
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
for (NSURL *URL in URLs) {
[[NSData data] writeToURL:URL atomically:YES];
}
cleanupBlocks = [NSMutableArray array];
for (NSURL *URL in URLs) {
[NSFileManager.defaultManager createDirectoryAtURL:URL.URLByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:NULL];
if (![[NSData data] writeToURL:URL atomically:YES]) {
NSLog(@"Could not touch log file at %@", URL);
}
}
RACSequence *paths = [URLs map:^(NSURL *URL) {
return URL.path;
}];
// We want to run any enqueued cleanup blocks after _and before_ each spec,
// in case beforeSuites (for example) use the fixtures.
void (^runCleanupBlocks)(void) = ^{
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
block();
}
NSTask *readShipIt = [self tailTaskWithPaths:paths];
[readShipIt launch];
[cleanupBlocks removeAllObjects];
};
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
[configuration beforeEach:^{
runCleanupBlocks();
}];
[configuration afterEach:^{
runCleanupBlocks();
SQRLKillAllTestApplications();
}];
atexit_b(^{
[readShipIt terminate];
});
}
QuickConfigurationEnd
- (void)setUp {
[super setUp];
@implementation QuickSpec (SQRLFixtures)
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
#pragma mark Lifecycle
Expecta.asynchronousTestTimeout = 3;
}
- (void)tearDown {
[super tearDown];
SQRLKillAllTestApplications();
}
- (void)addCleanupBlock:(dispatch_block_t)block {
[cleanupBlocks addObject:[block copy]];
[SQRLTestHelper addCleanupBlock:block];
}
#pragma mark Logging
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths {
NSPipe *outputPipe = [NSPipe pipe];
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
outputHandle.readabilityHandler = ^(NSFileHandle *handle) {
NSString *output = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding];
NSLog(@"\n%@", output);
};
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/tail";
task.standardOutput = outputPipe;
task.arguments = [[paths startWith:@"-f"] array];
return task;
}
#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 {
@@ -154,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;
@@ -163,6 +186,15 @@ QuickConfigurationEnd
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
[[NSData data] writeToURL:testAppLog atomically:YES];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSequence return:testAppLog.path]];
[readTestApp launch];
XCTAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
[readTestApp terminate];
}];
}
return fixtureURL;
@@ -211,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;
@@ -230,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;
@@ -253,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;
@@ -264,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);
@@ -273,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();
}
}
@@ -312,24 +344,20 @@ QuickConfigurationEnd
createInvocation = [NSString stringWithFormat:@"hdiutil create '%@' -fs 'HFS+' -volname '%@' -format UDSP -size 10m -srcfolder '%@' -quiet", destinationURL.path, name, directoryURL.path];
}
expect(@(system(createInvocation.UTF8String))).to(equal(@0));
expect(system(createInvocation.UTF8String)).to.equal(0);
NSString *mountInvocation = [NSString stringWithFormat:@"hdiutil attach '%@.sparseimage' -noverify -noautofsck -readwrite -quiet", destinationURL.path];
expect(@(system(mountInvocation.UTF8String))).to(equal(@0));
expect(system(mountInvocation.UTF8String)).to.equal(0);
NSString *path = [NSString stringWithFormat:@"/Volumes/%@", name];
[self addCleanupBlock:^{
NSString *detachInvocation = [NSString stringWithFormat:@"hdiutil detach '%@' -force -quiet", path];
expect(@(system(detachInvocation.UTF8String))).to(equal(@0));
expect(system(detachInvocation.UTF8String)).to.equal(0);
}];
return [NSURL fileURLWithPath:path isDirectory:YES];
}
- (BOOL)isRunningOnTravis {
return NSProcessInfo.processInfo.environment[@"TRAVIS"] != nil;
}
@end
#pragma clang diagnostic pop
+17
View File
@@ -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
+42
View File
@@ -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
+14 -19
View File
@@ -6,27 +6,22 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(SQRLUpdateSpec)
SpecBegin(SQRLUpdate)
it(@"should return nil when initialised without a url", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{} error:&error];
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
});
it(@"should return nil when initialised with a url not in URL syntax", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{ @"updateURL": [NSURL URLWithString:@"test"] } error:&error];
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
});
it(@"should validate release name and notes", ^{
@@ -37,20 +32,20 @@ it(@"should validate release name and notes", ^{
@"releaseNotes": [[NSObject alloc] init]
} error:NULL];
expect(update).notTo(beNil());
expect(update.updateURL).to(equal(updateURL));
expect(update.releaseName).to(beNil());
expect(update.releaseNotes).to(beNil());
expect(update).notTo.beNil();
expect(update.updateURL).to.equal(updateURL);
expect(update.releaseName).to.beNil();
expect(update.releaseNotes).to.beNil();
});
it(@"should parse Central style dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:NULL];
expect(update.releaseDate).to(equal([NSDate dateWithTimeIntervalSince1970:1379438667]));
expect(update.releaseDate).notTo.beNil();
});
it(@"should parse ISO 8601 dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:NULL];
expect(update.releaseDate).to(equal([NSDate dateWithTimeIntervalSince1970:1379506627]));
expect(update.releaseDate).notTo.beNil();
});
QuickSpecEnd
SpecEnd
+47 -66
View File
@@ -6,44 +6,38 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "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,34 +55,30 @@ beforeEach(^{
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should not install a corrupt update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@@ -98,16 +88,14 @@ describe(@"updating", ^{
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(app.terminated).will.beTruthy();
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should update to the most recently enqueued job", ^{
SKIP_IF_RUNNING_ON_TRAVIS
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
@@ -131,13 +119,11 @@ describe(@"updating", ^{
writeUpdate(update);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
SKIP_IF_RUNNING_ON_TRAVIS
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
@@ -145,22 +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 + SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager storageURL] first];
expect(appSupportURL).notTo(beNil());
appSupportURL = [[directoryManager applicationSupportURL] first];
expect(appSupportURL).notTo.beNil();
updateDirectoryURLs = [[RACSignal
defer:^{
@@ -175,25 +163,22 @@ describe(@"updating", ^{
});
it(@"should remove downloaded archives after updating", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateRequestCount": @2 });
expect([updateDirectoryURLs toArray]).toEventuallyNot(equal(@[]));
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([updateDirectoryURLs toArray]).toEventually(equal(@[]));
expect([updateDirectoryURLs toArray]).will.equal(@[]);
});
});
});
@@ -202,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];
});
@@ -219,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", ^{
@@ -236,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);
});
});
@@ -253,8 +238,6 @@ static RACSignal * (^stateNotificationListener)(void) = ^ {
describe(@"state", ^{
it(@"should transition through idle, checking and idle, when there is no update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
@@ -267,14 +250,12 @@ describe(@"state", ^{
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateIdle),
];
expect(states).toEventually(equal(expectedStates));
expect(states).will.equal(expectedStates);
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(testApplication.terminated).will.beTruthy();
});
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
@@ -285,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);
@@ -298,10 +279,10 @@ describe(@"state", ^{
@(SQRLUpdaterStateDownloadingUpdate),
@(SQRLUpdaterStateAwaitingRelaunch),
];
expect(states).withTimeout(SQRLLongTimeout).toEventually(equal(expectedStates));
expect(states).will.equal(expectedStates);
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(testApplication.terminated).will.beTruthy();
});
});
QuickSpecEnd
SpecEnd
+20 -26
View File
@@ -6,44 +6,38 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
QuickSpecBegin(SQRLZipArchiverSpec)
SpecBegin(SQRLZipArchiver)
it(@"should extract a zip archive created by the Finder", ^{
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication 2.1.app"];
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should fail to extract a nonexistent zip archive", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:[self.temporaryDirectoryURL URLByAppendingPathComponent:@"foo.zip"] intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
NSLog(@"%@", error);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLZipArchiverErrorDomain));
expect(@(error.code)).to(equal(@(SQRLZipArchiverShellTaskFailed)));
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo(equal(0));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
});
it(@"should create a zip archive readable by itself", ^{
@@ -51,21 +45,21 @@ it(@"should create a zip archive readable by itself", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zipURL fromDirectoryAtURL:self.testApplicationURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@([NSFileManager.defaultManager fileExistsAtPath:zipURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:zipURL.path]).to.beTruthy();
success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app"];
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+1 -1
View File
@@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
+28
View File
@@ -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
-19
View File
@@ -1,19 +0,0 @@
//
// SwiftSpec.swift
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2014-10-14.
// Copyright (c) 2014 GitHub. All rights reserved.
//
import Foundation
import Nimble
import Quick
// Without this, the Swift stdlib won't be linked into the test target (even if
// “Embedded Content Contains Swift Code” is enabled).
class SwiftSpec: QuickSpec {
override func spec() {
expect(true).to(beTruthy())
}
}
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>rules</key>
<dict>
<key>.*</key>
<true/>
</dict>
</dict>
</plist>
+3 -6
View File
@@ -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,9 +72,6 @@
__block NSUInteger updateCheckCount = 1;
NSInteger updateRequestCount = [NSProcessInfo.processInfo.environment[@"SQRLUpdateRequestCount"] integerValue];
if (updateRequestCount < 1) updateRequestCount = 1;
[[[[[[[[[[RACSignal
defer:^{
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
@@ -93,7 +90,7 @@
return testUpdate.final;
}]
take:updateRequestCount]
take:1]
doNext:^(id _) {
NSLog(@"***** READY TO INSTALL UPDATE *****");
}]
+1 -3
View File
@@ -9,7 +9,7 @@
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>com.github.Squirrel.${PRODUCT_NAME:rfc1034identifier}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
@@ -30,7 +30,5 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<false/>
</dict>
</plist>
+1 -1
View File
@@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>${PRODUCT_NAME}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
+2 -9
View File
@@ -10,7 +10,7 @@ config ()
{
# A whitespace-separated list of executables that must be present and locatable.
: ${REQUIRED_TOOLS="xctool"}
export REQUIRED_TOOLS
}
@@ -28,14 +28,7 @@ main ()
check_deps
fi
local submodules=$(git submodule status)
local result=$?
if [ "$result" -ne "0" ]
then
exit $result
fi
local submodules=$(git submodule status 2>/dev/null)
if [ -n "$submodules" ]
then
echo "*** Updating submodules..."
BIN
View File
Binary file not shown.
+19 -26
View File
@@ -41,26 +41,30 @@ config ()
# Individual names can be quoted to avoid word splitting.
: ${SCHEMES:=Squirrel}
# A source-able file with information about the Keychain, containing our
# code-signing certificate.
: ${KEYCHAIN_INFO:=/var/lib/jenkins/config/xcodekeychain}
export XCWORKSPACE
export XCODEPROJ
export BOOTSTRAP
export XCTOOL_OPTIONS
export SCHEMES
export KEYCHAIN_INFO
}
##
## Code Signing
##
setup_keychain ()
unlock_keychain ()
{
KEY_CHAIN=build.keychain
security create-keychain -p travis $KEY_CHAIN
security default-keychain -s $KEY_CHAIN
security unlock-keychain -p travis $KEY_CHAIN
security set-keychain-settings -t 3600 -u $KEY_CHAIN
security import script/certs.p12 -k build.keychain -P $KEY_PASSWORD -T /usr/bin/codesign
if [ -e "$KEYCHAIN_INFO" ]
then
# Unlock the keychain so the certificates it contains can be used for building.
. "$KEYCHAIN_INFO"
security unlock-keychain -p "$XCODE_KEYCHAIN_PASSWORD" "$XCODE_KEYCHAIN"
fi
}
##
@@ -70,7 +74,7 @@ setup_keychain ()
main ()
{
config
setup_keychain
unlock_keychain
if [ -f "$BOOTSTRAP" ]
then
@@ -136,29 +140,18 @@ build_scheme ()
local awkstatus=$?
if [ "$awkstatus" -eq "1" ]
then
# SDK not found, try for iphonesimulator.
sdkflag="-sdk iphonesimulator"
# Determine whether the unit tests will run with iphonesimulator
run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build
awkstatus=$?
if [ "$awkstatus" -ne "0" ]
then
# Unit tests will not run on iphonesimulator.
sdkflag=""
fi
fi
if [ "$awkstatus" -ne "0" ]
then
# Unit tests aren't supported.
action=build
fi
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env ruby
def exit_error(description)
puts "{ \"error\": \"#{description}\" }"
exit 1
end
module Targets
class Object
def self.match(path)
!File.symlink?(path)
end
attr_reader :path
def initialize(path)
@path = path
end
def search_directories
[]
end
end
class Bundle < Object
def self.match(path)
return false unless super
return false unless match = File.extname(path).match(/\.(.+)/)
return match[1] == extension
end
def contents_directories
[ "Contents" ]
end
def suffix_directories
[
"Frameworks",
"SharedFrameworks",
"XPCServices",
"PlugIns",
"Support",
"SharedSupport",
File.join("Library", "LoginItems"),
File.join("Library", "LaunchServices"),
]
end
def search_directories
contents_directories.product(suffix_directories).map do |combination|
File.join(*combination)
end
end
end
class VersionedBundle < Bundle
def contents_directories
contents_directories = []
versions_directory = File.join(@path, "Versions")
Dir.foreach(versions_directory) do |current_version|
next if current_version.start_with?('.')
full_path = File.join(versions_directory, current_version)
next if File.symlink?(full_path)
contents_directories << File.join("Versions", current_version)
end
contents_directories
end
end
class App < Bundle
def self.extension
"app"
end
end
class Framework < VersionedBundle
def self.extension
"framework"
end
end
class XPCService < Bundle
def self.extension
"xpc"
end
end
class Tool < Object
def self.match(path)
return false unless super
return false unless File.executable?(path)
# Look for an __TEXT,__info_plist section
# This contains the CFBundleIdentifier necessary for signing
`otool -l #{path}` =~ /__info_plist/
end
end
def self.all_targets
[ App, Framework, XPCService, Tool ]
end
end
module Codesign
class Task
def self.sign(target, identity)
codesign_with_arguments([ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,resource-rules,requirements", "--verbose", target.path ])
end
protected
def self.codesign_with_arguments(arguments)
$stdout.flush
pid = fork
if pid.nil?
exec("/usr/bin/xcrun", *([ "codesign" ].concat(arguments)))
else
Process.wait(pid)
exit_error("codesign failed") unless $?.exitstatus == 0
end
end
end
end
codesign_identity = ENV['CODE_SIGN_IDENTITY']
unless codesign_identity
exit_error("No codesign identity provided")
end
build_dir = ENV['CONFIGURATION_BUILD_DIR']
product_name = ENV['FULL_PRODUCT_NAME']
unless build_dir && product_name
exit_error("Path to built product not provided")
end
input_path = File.join(build_dir, product_name)
unless File.exists?(input_path)
exit_error("Built product doesn't exist at path provided")
end
targets = [ ]
def match_path(path)
Targets.all_targets.each do |target|
return target.new(path) if target.match(path)
end
nil
end
root_target = match_path input_path
if root_target.nil?
exit_error("Built product is not a known file structure, cannot deep codesign")
end
# Recursion base case can't use build_dir because there can be >1 product
paths_to_search = root_target.search_directories.map do |current_dir|
File.join(root_target.path, current_dir)
end
while paths_to_search.size > 0
new_search_paths = []
paths_to_search.each do |search_path|
next unless File.directory?(search_path)
Dir.foreach(search_path) do |current_directory_entry|
next if current_directory_entry.start_with?(".")
target = match_path(File.join(search_path, current_directory_entry))
next if target.nil?
targets << target
new_search_paths << target.search_directories.map do |current_search_directory|
File.join(target.path, current_search_directory)
end
end
end
paths_to_search = new_search_paths.flatten
end
puts "Signing these targets:"
puts "="*22
print "\n"
# Sort into nested-most first, outer targets might include inner targets
# in their ResourceRules
targets = targets.sort_by do |target|
target.path.size
end
targets.reverse!
targets.each do |target|
puts "=> #{target.path}"
Codesign::Task.sign(target, codesign_identity)
end
+3 -1
View File
@@ -2,8 +2,10 @@ BEGIN {
FS = "\n";
}
/Schemes:/ {
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}