Compare commits
238
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfbcdafae4 | ||
|
|
8f2c9d08b4 | ||
|
|
409d6ef781 | ||
|
|
8fb6d0e64f | ||
|
|
67cc4c2e14 | ||
|
|
feee9f7995 | ||
|
|
318b9a9081 | ||
|
|
217ada5c47 | ||
|
|
819aa8f815 | ||
|
|
f0f2523b5d | ||
|
|
283f9ef559 | ||
|
|
0f32a80019 | ||
|
|
c76b3158e7 | ||
|
|
ca72079898 | ||
|
|
625edb1388 | ||
|
|
48705686d0 | ||
|
|
5cd1c0ecd8 | ||
|
|
94c98f4e8e | ||
|
|
f1c4012aa0 | ||
|
|
bb370d2b42 | ||
|
|
eb8ef9cd48 | ||
|
|
dad7bdfa89 | ||
|
|
92c37377ca | ||
|
|
c65597ee6f | ||
|
|
18c923f343 | ||
|
|
5c9702f1d4 | ||
|
|
7850a0707b | ||
|
|
8a3bb3d896 | ||
|
|
40cf893899 | ||
|
|
539c7983c8 | ||
|
|
a13090740d | ||
|
|
f82fbaf53d | ||
|
|
ef2bc2345a | ||
|
|
e74d412eb9 | ||
|
|
b8ece2fc80 | ||
|
|
edb4aafa31 | ||
|
|
de276d73a6 | ||
|
|
7655bd0a90 | ||
|
|
e9a5874339 | ||
|
|
1ac9c4ddc1 | ||
|
|
9907d44f40 | ||
|
|
17c7238489 | ||
|
|
b5bbdfbed0 | ||
|
|
28079454dc | ||
|
|
210e09e079 | ||
|
|
f0161a51d9 | ||
|
|
d121f4cf7f | ||
|
|
02bb683cf7 | ||
|
|
d64a09bb2b | ||
|
|
5f201f833e | ||
|
|
6e1b3c9ded | ||
|
|
1109d19a9e | ||
|
|
2c567384db | ||
|
|
fcc86ca470 | ||
|
|
7af569196e | ||
|
|
70e7436e11 | ||
|
|
086a23661d | ||
|
|
e1460343ff | ||
|
|
faccaf5f6f | ||
|
|
1f8d6e87cc | ||
|
|
a70db2793f | ||
|
|
cbb75b1495 | ||
|
|
df49ea7aca | ||
|
|
2d8ea97081 | ||
|
|
d07c2d4d9d | ||
|
|
89ee65156f | ||
|
|
58062408d7 | ||
|
|
46d8fa7fa1 | ||
|
|
645d276fc3 | ||
|
|
25cb739760 | ||
|
|
14ff8e2012 | ||
|
|
4826034831 | ||
|
|
35081231a3 | ||
|
|
aa1db49ca0 | ||
|
|
173dd52f1c | ||
|
|
b751e75e19 | ||
|
|
789feef723 | ||
|
|
f4af70b715 | ||
|
|
22c1a8dc07 | ||
|
|
79dab822fd | ||
|
|
5792209feb | ||
|
|
1075abadec | ||
|
|
9ac9e32509 | ||
|
|
f1d818b321 | ||
|
|
7834599175 | ||
|
|
bf4355a522 | ||
|
|
656decb128 | ||
|
|
4ef110e84b | ||
|
|
a22ebbfdb9 | ||
|
|
8cdf3d0666 | ||
|
|
ff6a2e1cb7 | ||
|
|
c3a41e9032 | ||
|
|
27eb479de4 | ||
|
|
aab4ca3efb | ||
|
|
6bc3b0d466 | ||
|
|
ef83ed3175 | ||
|
|
c382fbce40 | ||
|
|
d0498c528e | ||
|
|
cb2502510b | ||
|
|
49a8f3b624 | ||
|
|
36e3b17abb | ||
|
|
eb49de653b | ||
|
|
35e5a03130 | ||
|
|
b4387c312a | ||
|
|
52f9eb8f4f | ||
|
|
cfc5bbf25f | ||
|
|
aab4fc5d59 | ||
|
|
ae6db20fa7 | ||
|
|
2010092d95 | ||
|
|
0166b2fef1 | ||
|
|
f80f67534c | ||
|
|
0b677cefc1 | ||
|
|
d63ff8291e | ||
|
|
742f46ff8f | ||
|
|
1e76fd3c15 | ||
|
|
856ac9f30f | ||
|
|
e4ac04cb2b | ||
|
|
4640763f1b | ||
|
|
3b3408d809 | ||
|
|
d93f7d0ae8 | ||
|
|
bdb4447511 | ||
|
|
4e36d604a0 | ||
|
|
5bfdb4683c | ||
|
|
860c010a06 | ||
|
|
84652b5f5c | ||
|
|
a8e3bf6b30 | ||
|
|
3f3856f38b | ||
|
|
aa6ffea547 | ||
|
|
843f8adeb0 | ||
|
|
93df075824 | ||
|
|
4d061c2138 | ||
|
|
854a4181ff | ||
|
|
2f75b436c7 | ||
|
|
6109b415f3 | ||
|
|
30027e933e | ||
|
|
0a198ed1da | ||
|
|
ab93693ae0 | ||
|
|
12ea8e209a | ||
|
|
931d19bc72 | ||
|
|
fc41d39585 | ||
|
|
a28abecb4d | ||
|
|
8ba490e5d3 | ||
|
|
be824ed1fb | ||
|
|
cb06bd800e | ||
|
|
6c18360388 | ||
|
|
d6356b0255 | ||
|
|
5e4e7d6fbc | ||
|
|
bb56dc4c87 | ||
|
|
a91faeaa6a | ||
|
|
e5b241dfc1 | ||
|
|
17975dd64e | ||
|
|
33ee8ca29d | ||
|
|
a9882a73d0 | ||
|
|
0e9ed45112 | ||
|
|
1e59131f03 | ||
|
|
c86dfb14c3 | ||
|
|
fcb1810178 | ||
|
|
0126d18c00 | ||
|
|
6abd2a6c82 | ||
|
|
7532c29e8e | ||
|
|
f3c96e219c | ||
|
|
6a85e07b93 | ||
|
|
57f339b2e4 | ||
|
|
8c303fb371 | ||
|
|
b3f397b81e | ||
|
|
10e7af2f81 | ||
|
|
a71b6f5efe | ||
|
|
df45644b6f | ||
|
|
f4492c20a5 | ||
|
|
2b5b334903 | ||
|
|
32cccdc77a | ||
|
|
8247064cb6 | ||
|
|
c20c540e8a | ||
|
|
07cb7a07c9 | ||
|
|
b35b602824 | ||
|
|
f400da11f3 | ||
|
|
14a36b50a4 | ||
|
|
98cc37d907 | ||
|
|
e0cf250d7b | ||
|
|
cf8b4bb853 | ||
|
|
8f870954a6 | ||
|
|
871379d9e0 | ||
|
|
c13c8de24d | ||
|
|
01f4acfe5c | ||
|
|
0f9bd718a8 | ||
|
|
58b8376f54 | ||
|
|
d92516dee4 | ||
|
|
6f3ba37b19 | ||
|
|
69c05bf159 | ||
|
|
b35b11a634 | ||
|
|
07cd9c19a7 | ||
|
|
e5a9c2c740 | ||
|
|
1072ef3c16 | ||
|
|
7a23627711 | ||
|
|
2713404d29 | ||
|
|
d11f29346a | ||
|
|
f5e7c52ba8 | ||
|
|
704933eaa1 | ||
|
|
db416ac0f0 | ||
|
|
f2f3f9922a | ||
|
|
4f114dc343 | ||
|
|
bf64476b13 | ||
|
|
b31f881103 | ||
|
|
665b947a1e | ||
|
|
36b5feec15 | ||
|
|
85309ede82 | ||
|
|
d9e37ab259 | ||
|
|
fed28478e4 | ||
|
|
4b9c02563f | ||
|
|
94837037f6 | ||
|
|
d54b0c2bfa | ||
|
|
21bd5012d6 | ||
|
|
4ffe08f9c6 | ||
|
|
a314caa355 | ||
|
|
6819ce4215 | ||
|
|
efba16e355 | ||
|
|
af8694a8f6 | ||
|
|
4c562a7c2e | ||
|
|
f2c534ff30 | ||
|
|
937319e9f5 | ||
|
|
ef28defe9c | ||
|
|
1561118861 | ||
|
|
755d50263d | ||
|
|
97ecb1bf72 | ||
|
|
b09a0cb3c8 | ||
|
|
8f9bd34e2b | ||
|
|
3b2e3639d1 | ||
|
|
dc861aba42 | ||
|
|
a12d5d5cda | ||
|
|
c4144ca55d | ||
|
|
a2dfe45177 | ||
|
|
7a667b8fc4 | ||
|
|
c516873458 | ||
|
|
6761505219 | ||
|
|
f3b4ba98a9 | ||
|
|
c194810749 | ||
|
|
6702f9515d | ||
|
|
39cf57582f |
+6
-19
@@ -1,22 +1,9 @@
|
||||
[submodule "External/ReactiveCocoa"]
|
||||
path = External/ReactiveCocoa
|
||||
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
|
||||
[submodule "External/Mantle"]
|
||||
path = External/Mantle
|
||||
url = https://github.com/github/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
|
||||
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
osx_image: xcode8.3
|
||||
language: objective-c
|
||||
|
||||
install: script/bootstrap
|
||||
script: script/cibuild
|
||||
notifications:
|
||||
email: false
|
||||
env:
|
||||
global:
|
||||
secure: L4C3hHzXc/sgL35dIz3kjqIzPWnXCHsFOgbOcz0f249zlnegeKHQdChm4HUDtfrbmKLMYQCIvVfcgPeLaHkCSeHqXUihXLT2LglK+0GX3MBbFzde9x1uln2O68hH1tTmBg2O2i4Dtc4D3e+2tVv3XmhQDZt0znIVO6n9YgevDqw=
|
||||
@@ -1,2 +0,0 @@
|
||||
github "Mantle/Mantle" ~> 1.5.3
|
||||
github "ReactiveCocoa/ReactiveCocoa" ~> 2.4.3
|
||||
@@ -1,3 +0,0 @@
|
||||
github "jspahrsummers/xcconfigs" ~> 0.8.1
|
||||
github "Quick/Quick" ~> 1.1.0
|
||||
github "Quick/Nimble" ~> 6.1.0
|
||||
@@ -1,5 +0,0 @@
|
||||
github "Mantle/Mantle" "1.5.8"
|
||||
github "Quick/Nimble" "v6.1.0"
|
||||
github "Quick/Quick" "v1.1.0"
|
||||
github "ReactiveCocoa/ReactiveCocoa" "v2.5"
|
||||
github "jspahrsummers/xcconfigs" "0.8.1"
|
||||
Vendored
-1
Submodule Carthage/Checkouts/Mantle deleted from 78d3966b3c
Vendored
-1
Submodule Carthage/Checkouts/Nimble deleted from e3c1a78af3
Vendored
-1
Submodule Carthage/Checkouts/Quick deleted from e4fa1e85c0
Vendored
-1
Submodule Carthage/Checkouts/ReactiveCocoa deleted from 32364b9040
Vendored
-1
Submodule Carthage/Checkouts/xcconfigs deleted from 99624a6af3
+1
Submodule External/Mantle added at 8aef509d18
Vendored
+1
-1
Submodule External/OHHTTPStubs updated: 08776e99bc...acaf8b8af7
+1
Submodule External/ReactiveCocoa added at 0138dabe60
@@ -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
|
||||
@@ -14,49 +13,23 @@ suitable update.
|
||||
|
||||
The update JSON Squirrel requests should be dynamically generated based on
|
||||
criteria in the request, and whether an update is required. Squirrel relies
|
||||
on server side support for determining whether an update is required, see
|
||||
[Server Support](#server-support).
|
||||
on server side support for determining whether an update is required, see [Server
|
||||
Support](#server-support) below.
|
||||
|
||||
Squirrel's installer is also designed to be fault tolerant, and ensure that any
|
||||
Squirrel’s installer is also designed to be fault tolerant, and ensure that any
|
||||
updates installed are valid.
|
||||
|
||||

|
||||
|
||||
# Adopting Squirrel
|
||||
|
||||
1. Install xctool with `brew install xctool`
|
||||
1. Add the Squirrel repository as a git submodule
|
||||
1. Run `script/bootstrap` from within the submodule
|
||||
1. Add references to Squirrel.xcodeproj and its [dependencies](#dependencies) to
|
||||
your project
|
||||
1. Add a reference to Squirrel.xcodeproj to your project
|
||||
1. Add Squirrel.framework as a target dependency
|
||||
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
|
||||
it into your Frameworks directory
|
||||
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
|
||||
does not embed them itself.
|
||||
|
||||
If you’re developing Squirrel on its own, then use `Squirrel.xcworkspace`.
|
||||
|
||||
# Dependencies
|
||||
|
||||
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
|
||||
and [Mantle](https://github.com/Mantle/Mantle).
|
||||
|
||||
If your application is already using ReactiveCocoa, ensure it is using the same
|
||||
version as Squirrel.
|
||||
|
||||
Otherwise, add a target dependency and Copy Files build phase entry for the
|
||||
ReactiveCocoa.framework target included in Squirrel's repository, in
|
||||
External/ReactiveCocoa.
|
||||
|
||||
Similarly, ensure your application includes Mantle, or copies in the Squirrel
|
||||
version.
|
||||
|
||||
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
|
||||
includes the directory that Squirrel.framework, ReactiveCocoa.framework
|
||||
and Mantle.framework are copied into.
|
||||
|
||||
# Configuration
|
||||
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
|
||||
Frameworks directory Squirrel.framework is copied into
|
||||
|
||||
Once Squirrel is added to your project, you need to configure and start it.
|
||||
|
||||
@@ -64,16 +37,7 @@ Once Squirrel is added to your project, you need to configure and start it.
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
NSURLComponents *components = [[NSURLComponents alloc] init];
|
||||
|
||||
components.scheme = @"http";
|
||||
components.host = @"mycompany.com";
|
||||
components.path = @"/myapp/latest";
|
||||
|
||||
NSString *bundleVersion = NSBundle.mainBundle.sqrl_bundleVersion;
|
||||
components.query = [[NSString stringWithFormat:@"version=%@", bundleVersion] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]
|
||||
|
||||
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:components.URL]];
|
||||
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://mycompany.com/myapp/latest"]]];
|
||||
|
||||
// Check for updates every 4 hours.
|
||||
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
|
||||
@@ -84,26 +48,7 @@ Squirrel will periodically request and automatically download any updates. When
|
||||
your application terminates, any downloaded update will be automatically
|
||||
installed.
|
||||
|
||||
## Update Requests
|
||||
|
||||
Squirrel is indifferent to the request the client application provides for
|
||||
update checking. `Accept: application/json` is added to the request headers
|
||||
because Squirrel is responsible for parsing the response.
|
||||
|
||||
For the requirements imposed on the responses and the body format of an update
|
||||
response see [Server Support](#server-support).
|
||||
|
||||
Your update request must *at least* include a version identifier so that the
|
||||
server can determine whether an update for this specific version is required. It
|
||||
may also include other identifying criteria such as operating system version or
|
||||
username, to allow the server to deliver as fine grained an update as you
|
||||
would like.
|
||||
|
||||
How you include the version identifier or other criteria is specific to the
|
||||
server that you are requesting updates from. A common approach is to use query
|
||||
parameters, [Configuration](#configuration) shows an example of this.
|
||||
|
||||
## Update Available Notifications
|
||||
## Update Notifications
|
||||
|
||||
To know when an update is ready to be installed, you can subscribe to the
|
||||
`updates` signal on `SQRLUpdater`:
|
||||
@@ -114,18 +59,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) {
|
||||
@@ -133,33 +72,17 @@ subscribe to the `relaunchToInstallUpdate` signal on `SQRLUpdater`:
|
||||
}];
|
||||
```
|
||||
|
||||
# Server Support
|
||||
# Update JSON Format
|
||||
|
||||
Your server should determine whether an update is required based on the
|
||||
[Update Request](#update-requests) your client issues.
|
||||
|
||||
If an update is required your server should respond with a status code of
|
||||
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
|
||||
[update JSON](#update-json-format) in the body. Squirrel **will** download and
|
||||
install this update, even if the version of the update is the same as the
|
||||
currently running version. To save redundantly downloading the same version
|
||||
multiple times your server must not inform the client to update.
|
||||
|
||||
If no update is required your server must respond with a status code of
|
||||
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
|
||||
will check for an update again at the interval you specify.
|
||||
|
||||
## Update JSON Format
|
||||
|
||||
When an update is available, Squirrel expects the following schema in response
|
||||
to the update request provided:
|
||||
Squirrel requests the URL you provide with `Accept: application/json` and
|
||||
expects the following schema in response:
|
||||
|
||||
```json
|
||||
{
|
||||
"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",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -170,10 +93,23 @@ installing ZIP updates. If future update formats are supported their MIME type
|
||||
will be added to the `Accept` header so that your server can return the
|
||||
appropriate format.
|
||||
|
||||
"pub_date" if present must be formatted according to ISO 8601.
|
||||
"pub_date" if present must be formatted according to ISO 8601
|
||||
|
||||
# Server Support
|
||||
|
||||
If an update is required your server should respond with a status code of
|
||||
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
|
||||
update JSON in the body. Squirrel **will** download and install this update,
|
||||
even if the version of the update is the same as the currently running version.
|
||||
To save redundantly downloading the same version multiple times your server must
|
||||
inform the client not to update.
|
||||
|
||||
If no update is required your server must respond with a status code of
|
||||
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
|
||||
will check for an update again at the interval you specify.
|
||||
|
||||
# User Interface
|
||||
|
||||
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).
|
||||
|
||||
+769
-518
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0830"
|
||||
version = "2.0">
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
@@ -22,14 +22,14 @@
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "NO"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "NO">
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
BuildableName = "SquirrelTests.xctest"
|
||||
BuildableName = "SquirrelTests.octest"
|
||||
BlueprintName = "SquirrelTests"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
@@ -37,61 +37,32 @@
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Test">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
BuildableName = "SquirrelTests.xctest"
|
||||
BuildableName = "SquirrelTests.octest"
|
||||
BlueprintName = "SquirrelTests"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
|
||||
BuildableName = "Squirrel.framework"
|
||||
BlueprintName = "Squirrel"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "FORCE_APP_IDENTIFIER"
|
||||
value = "com.github.Squirrel.SquirrelTests"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = ""
|
||||
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugXPCServices = "NO"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
|
||||
BuildableName = "Squirrel.framework"
|
||||
BlueprintName = "Squirrel"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "SQRLUpdateFromURL"
|
||||
@@ -103,20 +74,11 @@
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Profile"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
|
||||
BuildableName = "Squirrel.framework"
|
||||
BlueprintName = "Squirrel"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0830"
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -23,10 +23,10 @@
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Test"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Test">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
@@ -38,21 +38,17 @@
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
|
||||
@@ -65,13 +61,12 @@
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableProductRunnable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0830"
|
||||
LastUpgradeVersion = "0500"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
@@ -20,45 +20,47 @@
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
|
||||
BuildableName = "SquirrelTests.octest"
|
||||
BlueprintName = "SquirrelTests"
|
||||
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>
|
||||
</Testables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Debug"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D014AC0017B97885007D79D0"
|
||||
BuildableName = "ShipIt"
|
||||
BlueprintName = "ShipIt"
|
||||
ReferencedContainer = "container:Squirrel.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
buildConfiguration = "Release"
|
||||
debugDocumentVersioning = "YES">
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Squirrel.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/Mantle/Mantle.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/ReactiveCocoa/ReactiveCocoa.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/Quick/Quick.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -14,7 +14,4 @@
|
||||
// Info.plist, or nil if the key is not present.
|
||||
@property (nonatomic, copy, readonly) NSString *sqrl_bundleVersion;
|
||||
|
||||
/// The value of the `kCFBundleExecutableKey` key.
|
||||
@property (nonatomic, copy, readonly) NSString *sqrl_executableName;
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,8 +14,4 @@
|
||||
return [self objectForInfoDictionaryKey:(id)kCFBundleVersionKey];
|
||||
}
|
||||
|
||||
- (NSString *)sqrl_executableName {
|
||||
return [self objectForInfoDictionaryKey:(id)kCFBundleExecutableKey];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// NSData+SQRLExtensions.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSData (SQRLExtensions)
|
||||
|
||||
// Returns an NSData for the SHA1 hash of the receiver.
|
||||
- (NSData *)sqrl_SHA1Hash;
|
||||
|
||||
// Returns the base16 encoding of the receiver.
|
||||
- (NSString *)sqrl_base16String;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// NSData+SQRLExtensions.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+SQRLExtensions.h"
|
||||
|
||||
#import <CommonCrypto/CommonCrypto.h>
|
||||
|
||||
@implementation NSData (SQRLExtensions)
|
||||
|
||||
- (NSData *)sqrl_SHA1Hash {
|
||||
unsigned char hash[CC_SHA1_DIGEST_LENGTH];
|
||||
CC_SHA1(self.bytes, (CC_LONG)self.length, hash);
|
||||
return [NSData dataWithBytes:hash length:sizeof(hash) / sizeof(*hash)];
|
||||
}
|
||||
|
||||
- (NSString *)sqrl_base16String {
|
||||
NSMutableString *base16 = [NSMutableString stringWithCapacity:self.length * 2];
|
||||
uint8_t *bytes = (uint8_t *)self.bytes;
|
||||
for (NSUInteger idx = 0; idx < self.length; idx++) {
|
||||
uint8_t byte = *(bytes + idx);
|
||||
[base16 appendFormat:@"%02hhx", byte];
|
||||
}
|
||||
return base16;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// NSHTTPURLResponse+SQRLExtensions.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 04/12/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSHTTPURLResponse (SQRLExtensions)
|
||||
|
||||
// Case insensitively look up a header field value.
|
||||
//
|
||||
// Returns the value of the header if present or nil otherwise.
|
||||
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// NSHTTPURLResponse+SQRLExtensions.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 04/12/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSHTTPURLResponse+SQRLExtensions.h"
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@implementation NSHTTPURLResponse (SQRLExtensions)
|
||||
|
||||
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field {
|
||||
return [[[self.allHeaderFields.rac_signal
|
||||
filter:^ BOOL (RACTuple *keyValuePair) {
|
||||
return [keyValuePair[0] caseInsensitiveCompare:field] == NSOrderedSame;
|
||||
}]
|
||||
reduceEach:^(NSString *key, NSString *value) {
|
||||
return value;
|
||||
}]
|
||||
first];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// RACSignal+SQRLFileManagerExtensions.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@interface RACSignal (SQRLFileManagerExtensions)
|
||||
|
||||
- (RACSignal *)sqrl_tryCreateDirectory;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// RACSignal+SQRLFileManagerExtensions.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RACSignal+SQRLFileManagerExtensions.h"
|
||||
|
||||
@implementation RACSignal (SQRLFileManagerExtensions)
|
||||
|
||||
- (RACSignal *)sqrl_tryCreateDirectory {
|
||||
return [[self
|
||||
try:^(NSURL *directoryURL, NSError **errorRef) {
|
||||
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:nil error:errorRef];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -sqrl_tryCreateDirectory", self];
|
||||
}
|
||||
|
||||
@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.
|
||||
@@ -99,15 +97,12 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
|
||||
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
|
||||
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
|
||||
|
||||
return [RACDisposable disposableWithBlock:^{
|
||||
[subscriptionDisposable dispose];
|
||||
[transactionDisposable dispose];
|
||||
}];
|
||||
}] setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
|
||||
return [[RACSignal
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
[subscriber.disposable addDisposable:SQRLCreateTransaction(name, description)];
|
||||
[self subscribe:subscriber];
|
||||
}]
|
||||
setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
|
||||
}
|
||||
|
||||
- (RACSignal *)sqrl_addSubscriptionTransactionWithName:(NSString *)name description:(NSString *)descriptionFormat, ... {
|
||||
@@ -119,13 +114,13 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
|
||||
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
|
||||
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
|
||||
[transactionDisposable dispose];
|
||||
|
||||
return subscriptionDisposable;
|
||||
}] setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
|
||||
return [[RACSignal
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
|
||||
[self subscribe:subscriber];
|
||||
[transactionDisposable dispose];
|
||||
}]
|
||||
setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// SQRLAuthorization.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 06/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <Security/Security.h>
|
||||
|
||||
// Wraps an AuthorizationRef.
|
||||
@interface SQRLAuthorization : NSObject
|
||||
|
||||
// Designated initializer.
|
||||
//
|
||||
// authorization - Must not be NULL, the returned object assumes ownership of
|
||||
// the passed authorization.
|
||||
//
|
||||
// Returns an object which ties the `authorization` argument to the returned
|
||||
// object's lifetime.
|
||||
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
|
||||
|
||||
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
|
||||
|
||||
@end
|
||||
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// SQRLAuthorization.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 06/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLAuthorization.h"
|
||||
|
||||
@implementation SQRLAuthorization
|
||||
|
||||
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
|
||||
NSParameterAssert(authorization != NULL);
|
||||
|
||||
self = [self init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_authorization = authorization;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -21,44 +21,32 @@ extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// Implements the verification of Apple code signatures and requirements.
|
||||
// Implements the verification of Apple code signatures.
|
||||
@interface SQRLCodeSignature : MTLModel
|
||||
|
||||
// A serialized version of the `SecRequirementRef` that the receiver was
|
||||
// initialized with.
|
||||
@property (nonatomic, copy, readonly) NSData *requirementData;
|
||||
|
||||
// Determines the designated requirement of the currently-executing application.
|
||||
// The returned code signature can be used to verify that a bundle is code sign
|
||||
// valid and meets the designated requirement of the current application.
|
||||
// Determines the code signature of the currently-executing application.
|
||||
//
|
||||
// error - If not NULL, set to any error that occurs.
|
||||
//
|
||||
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
|
||||
// designated requirement for the running code.
|
||||
// signature for the running code.
|
||||
+ (instancetype)currentApplicationSignature:(NSError **)error;
|
||||
|
||||
// Determines the designated requirement of the specified bundle. The returned
|
||||
// code signature can be used to verify that an arbitrary bundle is valid and
|
||||
// meets the designated requirement of `bundle`.
|
||||
// Initializes the receiver with the given requirement.
|
||||
//
|
||||
// bundleURL - Must not be nil, the location of a bundle directory structure
|
||||
// which has been code signed and includes a designated requirement.
|
||||
// This bundle's designated requirement is used when verifying other
|
||||
// bundles.
|
||||
// error - If not NULL, set to any error that occurs.
|
||||
//
|
||||
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
|
||||
// designated requirement of the bundle at `bundleURL`.
|
||||
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)error;
|
||||
// requirement - The code requirement for tested bundles. This must not be NULL.
|
||||
- (id)initWithRequirement:(SecRequirementRef)requirement;
|
||||
|
||||
// Verifies the code signature of the specified bundle and verifies that the
|
||||
// bundle meets the receiver's requirement.
|
||||
// Verifies that the code signature of the specified bundle matches the receiver.
|
||||
//
|
||||
// bundleURL - The URL to the bundle to verify on disk. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send completed on success, or error
|
||||
// if the requirement was not verified successfully.
|
||||
// if the code signature was not verified successfully.
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL;
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,11 +7,8 @@
|
||||
//
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
|
||||
#import "EXTKeyPathCoding.h"
|
||||
#import "EXTScope.h"
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/RACSubscriber.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Security/Security.h>
|
||||
|
||||
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
|
||||
@@ -22,14 +19,9 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
@interface SQRLCodeSignature ()
|
||||
|
||||
// A `SecRequirementRef` that tested bundles must satisfy.
|
||||
@property (atomic, strong) id requirement;
|
||||
|
||||
// Initializes the receiver with the given requirement.
|
||||
//
|
||||
// This is the designated initializer for this class.
|
||||
//
|
||||
// requirement - The code requirement for tested bundles. This must not be NULL.
|
||||
- (id)initWithRequirement:(SecRequirementRef)requirement;
|
||||
// This property is automatically retained.
|
||||
@property (atomic) id requirement;
|
||||
|
||||
@end
|
||||
|
||||
@@ -45,49 +37,8 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
+ (instancetype)currentApplicationSignature:(NSError **)errorRef {
|
||||
SecCodeRef staticCode = NULL;
|
||||
OSStatus error = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
|
||||
if (error != noErr) {
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@onExit {
|
||||
CFRelease(staticCode);
|
||||
};
|
||||
|
||||
return [self signatureWithCode:staticCode error:errorRef];
|
||||
}
|
||||
|
||||
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)errorRef {
|
||||
SecStaticCodeRef bundleCode = NULL;
|
||||
OSStatus error = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &bundleCode);
|
||||
if (error != noErr) {
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@onExit {
|
||||
CFRelease(bundleCode);
|
||||
};
|
||||
|
||||
return [self signatureWithCode:bundleCode error:errorRef];
|
||||
}
|
||||
|
||||
+ (instancetype)signatureWithCode:(SecStaticCodeRef)code error:(NSError **)errorRef {
|
||||
SecRequirementRef designatedRequirement = NULL;
|
||||
OSStatus error = SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &designatedRequirement);
|
||||
if (error != noErr) {
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
@onExit {
|
||||
CFRelease(designatedRequirement);
|
||||
};
|
||||
|
||||
return [[SQRLCodeSignature alloc] initWithRequirement:designatedRequirement];
|
||||
+ (instancetype)currentApplicationSignature:(NSError **)error {
|
||||
return [self modelWithDictionary:nil error:error];
|
||||
}
|
||||
|
||||
- (id)initWithRequirement:(SecRequirementRef)requirement {
|
||||
@@ -98,12 +49,41 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionary error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
if (self.requirement == nil) {
|
||||
SecCodeRef staticCode = NULL;
|
||||
OSStatus result = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
|
||||
@onExit {
|
||||
if (staticCode != NULL) CFRelease(staticCode);
|
||||
};
|
||||
|
||||
if (result != noErr) {
|
||||
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
SecRequirementRef req = NULL;
|
||||
result = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &req);
|
||||
self.requirement = CFBridgingRelease(req);
|
||||
|
||||
if (result != noErr) {
|
||||
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Verification
|
||||
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
|
||||
return [[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
|
||||
return [[RACSignal create:^(id<RACSubscriber> subscriber) {
|
||||
SecStaticCodeRef staticCode = NULL;
|
||||
|
||||
OSStatus result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
|
||||
@@ -120,8 +100,10 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
|
||||
|
||||
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
|
||||
return nil;
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscriber.disposable.disposed) return;
|
||||
|
||||
CFErrorRef validityError = NULL;
|
||||
result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, (__bridge SecRequirementRef)self.requirement, &validityError);
|
||||
@@ -139,11 +121,10 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
|
||||
|
||||
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
|
||||
return nil;
|
||||
return;
|
||||
}
|
||||
|
||||
[subscriber sendCompleted];
|
||||
return nil;
|
||||
}] setNameWithFormat:@"-verifyCodeSignatureOfBundle: %@", bundleURL];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,16 +6,13 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// Provides the file locations that Squirrel/ShipIt use.
|
||||
@interface SQRLDirectoryManager : NSObject
|
||||
|
||||
// The application identifier to use in file locations.
|
||||
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
|
||||
|
||||
// Returns the shared `SQRLDirectoryManager` for the running application, based
|
||||
// on the bundle identifier or application name.
|
||||
+ (instancetype)currentApplicationManager;
|
||||
@@ -29,24 +26,27 @@
|
||||
// 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 subdirectory for the receiver’s
|
||||
// application identifier.
|
||||
//
|
||||
// Returns a signal which synchronously sends a URL then completes, or errors.
|
||||
- (RACSignal *)storageURL;
|
||||
@property (readonly, nonatomic) RACSignal *applicationSupportURL;
|
||||
|
||||
// Finds or creates a downloads directory to store update downloads in prior to
|
||||
// installation. This is an applicationSupportURL subdirectory.
|
||||
//
|
||||
// Returns a signal which sends a URL then completes, or errors.
|
||||
@property (readonly, nonatomic) RACSignal *downloadDirectoryURL;
|
||||
|
||||
// Creates a new directory to unpack a downloaded update into prior to
|
||||
// installation. This is an applicationSupportURL subdirectory.
|
||||
//
|
||||
// Returns a signal which sends a URL then completes, or errors.
|
||||
- (RACSignal *)createUniqueUpdateDirectoryURL;
|
||||
|
||||
// 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;
|
||||
@property (readonly, nonatomic) RACSignal *shipItStateURL;
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,8 +7,16 @@
|
||||
//
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import "RACSignal+SQRLFileManagerExtensions.h"
|
||||
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
@interface SQRLDirectoryManager ()
|
||||
|
||||
// The application identifier to use in file locations.
|
||||
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLDirectoryManager
|
||||
|
||||
@@ -24,7 +32,7 @@
|
||||
// Should only fallback to when running under otest, where
|
||||
// NSBundle.mainBundle doesn't return useful data.
|
||||
if (identifier == nil) {
|
||||
identifier = NSProcessInfo.processInfo.environment[@"FORCE_APP_IDENTIFIER"];
|
||||
identifier = NSRunningApplication.currentApplication.localizedName;
|
||||
}
|
||||
|
||||
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
|
||||
@@ -47,52 +55,74 @@
|
||||
|
||||
#pragma mark Folder URLs
|
||||
|
||||
- (RACSignal *)storageURL {
|
||||
return [[RACSignal
|
||||
+ (NSString *)fileSystemNameForIdentifier:(NSString *)identifier {
|
||||
// Periods are problematic in the filesystem because they denote file type.
|
||||
// A directory can become a package which can be undesirable.
|
||||
return [identifier stringByReplacingOccurrencesOfString:@"." withString:@"~"];
|
||||
}
|
||||
|
||||
- (RACSignal *)applicationSupportURL {
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSError *error = nil;
|
||||
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
|
||||
if (folderURL == nil) {
|
||||
NSURL *directoryURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
|
||||
if (directoryURL == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
folderURL = [folderURL URLByAppendingPathComponent:self.applicationIdentifier];
|
||||
if (![NSFileManager.defaultManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:folderURL];
|
||||
return [RACSignal return:directoryURL];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -storageURL", self];
|
||||
map:^ (NSURL *directoryURL) {
|
||||
RACSignal *identifierComponents = [@[ self.applicationIdentifier, @"com.github.Squirrel" ].rac_signal
|
||||
map:^(NSString *name) {
|
||||
return [self.class fileSystemNameForIdentifier:name];
|
||||
}];
|
||||
|
||||
RACSignal *pathComponents = [directoryURL.pathComponents.rac_signal concat:identifierComponents];
|
||||
return [NSURL fileURLWithPathComponents:[pathComponents array]];
|
||||
}]
|
||||
sqrl_tryCreateDirectory]
|
||||
setNameWithFormat:@"%@ -applicationSupportURL", self];
|
||||
}
|
||||
|
||||
- (RACSignal *)downloadDirectoryURL {
|
||||
return [[[[self
|
||||
applicationSupportURL]
|
||||
map:^(NSURL *directoryURL) {
|
||||
return [directoryURL URLByAppendingPathComponent:@"downloads"];
|
||||
}]
|
||||
sqrl_tryCreateDirectory]
|
||||
setNameWithFormat:@"%@ -downloadDirectoryURL", self];
|
||||
}
|
||||
|
||||
- (RACSignal *)createUniqueUpdateDirectoryURL {
|
||||
return [[[[self
|
||||
applicationSupportURL]
|
||||
map:^(NSURL *directoryURL) {
|
||||
// noindex so that Spotlight doesn't pick up apps pending update and
|
||||
// add them to the Launch Services database.
|
||||
return [[directoryURL URLByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString] URLByAppendingPathExtension:@"noindex"];
|
||||
}]
|
||||
try:^(NSURL *directoryURL, NSError **errorRef) {
|
||||
// Explicitly just provide the owner with permission, discarding the
|
||||
// current umask. This matches the `mkdtemp` behaviour.
|
||||
NSDictionary *directoryAttributes = @{
|
||||
NSFilePosixPermissions: @(S_IRWXU),
|
||||
};
|
||||
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:directoryAttributes error:errorRef];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -createUniqueUpdateDirectoryURL", 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 {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// SQRLDownload+Private.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDownload.h"
|
||||
|
||||
@interface SQRLDownload ()
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// request - Must not be nil. The request this download is for.
|
||||
// fileURL - Must not be nil. The file location to save received data to.
|
||||
//
|
||||
// Returns an initialised download suitable for saving response data.
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// SQRLDownload.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/11/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// A download is a file which holds the response body for a request and can be
|
||||
// resumed by a subsequent request if the initial transfer is interrupted.
|
||||
//
|
||||
// Don't create a download directly, instead retrieve one from
|
||||
// `SQRLDownloadManager`.
|
||||
@interface SQRLDownload : MTLModel
|
||||
|
||||
// The `request` the receiver was initialised with.
|
||||
@property (readonly, copy, nonatomic) NSURLRequest *request;
|
||||
|
||||
// The `fileURL` the receiver was initialised with.
|
||||
@property (readonly, copy, nonatomic) NSURL *fileURL;
|
||||
|
||||
// Calculate a request suitable for resuming the download.
|
||||
//
|
||||
// Returns a signal which sends an `NSURLRequest` then completes.
|
||||
@property (readonly, nonatomic) RACSignal *resumableRequest;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// SQRLDownload.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 21/11/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDownload.h"
|
||||
#import "SQRLDownload+Private.h"
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@implementation SQRLDownload
|
||||
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.request): request,
|
||||
@keypath(self.fileURL): fileURL,
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
- (RACSignal *)resumableRequest {
|
||||
return [[RACSignal
|
||||
return:self.request]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// SQRLDownloadController.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 26/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class SQRLDownload;
|
||||
@class SQRLResumableDownload;
|
||||
@class RACSignal;
|
||||
@class SQRLDirectoryManager;
|
||||
|
||||
// Stores metadata for download resumption, and manages the disk locations for
|
||||
// where they're stored.
|
||||
@interface SQRLDownloadManager : NSObject
|
||||
|
||||
// Default download manager, initializes a download manager with the current
|
||||
// application directory manager. Stores downloads in
|
||||
// `directoryManager.downloadDirectoryURL`.
|
||||
+ (instancetype)defaultDownloadManager;
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// directoryManager - Must not be nil.
|
||||
//
|
||||
// Returns a download manager which can resume previously started downloads for
|
||||
// the initialised directory manager's application.
|
||||
- (instancetype)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
|
||||
|
||||
// Clean the resumable download state, removes downloaded data and tracking
|
||||
// state.
|
||||
//
|
||||
// Returns a signal which errors or completes when all file operations have been
|
||||
// attempted.
|
||||
- (RACSignal *)removeAllResumableDownloads;
|
||||
|
||||
// Retrieve a previously started download, or initialise a new download.
|
||||
//
|
||||
// Previously started downloads may have been removed and require
|
||||
// reinitialising.
|
||||
//
|
||||
// request - The request you are performing and require a disk location to save
|
||||
// the response body to. Must not be nil.
|
||||
//
|
||||
// Returns a signal which sends an `SQRLDownload` object then completes, or
|
||||
// errors.
|
||||
- (RACSignal *)downloadForRequest:(NSURLRequest *)request;
|
||||
|
||||
// Store metadata for a download so that it can be resumed later.
|
||||
//
|
||||
// download - The download being stored for future resumption. Must not be nil.
|
||||
// request - The request for which the download can be resumed. Must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a signal which completes or errors.
|
||||
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,239 @@
|
||||
//
|
||||
// SQRLDownloadController.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 26/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDownloadManager.h"
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLDownload.h"
|
||||
#import "SQRLDownload+Private.h"
|
||||
#import "SQRLResumableDownload.h"
|
||||
|
||||
#import "NSData+SQRLExtensions.h"
|
||||
|
||||
@interface SQRLDownloadManager ()
|
||||
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
|
||||
@property (nonatomic, assign, readonly) dispatch_queue_t queue;
|
||||
|
||||
// Reads the download index.
|
||||
//
|
||||
// Returns a signal which sends the download index at time of read then
|
||||
// completes, or errors.
|
||||
- (RACSignal *)readDownloadIndex;
|
||||
|
||||
// Write a new download index.
|
||||
//
|
||||
// block - Map from the old download index to the new download index. Writing
|
||||
// is serialised, subsequent writers will receive the output of the
|
||||
// previous map block. If the output of the map is equal to the input
|
||||
// no write is attempted and the returned signal completes.
|
||||
//
|
||||
// Returns a signal which completes or errors.
|
||||
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block;
|
||||
@end
|
||||
|
||||
@implementation SQRLDownloadManager
|
||||
|
||||
+ (instancetype)defaultDownloadManager {
|
||||
static SQRLDownloadManager *defaultDownloadManager;
|
||||
static dispatch_once_t defaultDownloadManagerPredicate;
|
||||
|
||||
dispatch_once(&defaultDownloadManagerPredicate, ^{
|
||||
defaultDownloadManager = [[self alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
|
||||
});
|
||||
|
||||
return defaultDownloadManager;
|
||||
}
|
||||
|
||||
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
|
||||
NSParameterAssert(directoryManager != nil);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_directoryManager = directoryManager;
|
||||
|
||||
_queue = dispatch_queue_create("com.github.Squirrel.SQRLDownloadManager.queue", DISPATCH_QUEUE_CONCURRENT);
|
||||
dispatch_set_target_queue(_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_queue != NULL) dispatch_release(_queue);
|
||||
}
|
||||
|
||||
- (RACSignal *)removeAllResumableDownloads {
|
||||
return [[[RACSignal
|
||||
merge:@[
|
||||
[self downloadStoreIndexFileLocation],
|
||||
[self.directoryManager downloadDirectoryURL]
|
||||
]]
|
||||
flattenMap:^ (NSURL *location) {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:location error:&error];
|
||||
return (remove ? [RACSignal empty] : [RACSignal error:error]);
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
|
||||
return [RACSignal error:error];
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
- (RACSignal *)downloadStoreIndexFileLocation {
|
||||
return [[[self.directoryManager
|
||||
applicationSupportURL]
|
||||
map:^(NSURL *directory) {
|
||||
return [directory URLByAppendingPathComponent:@"DownloadIndex.plist"];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
- (RACSignal *)readDownloadIndex {
|
||||
return [[[self
|
||||
downloadStoreIndexFileLocation]
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [RACSignal
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
dispatch_async(self.queue, ^{
|
||||
if (subscriber.disposable.disposed) return;
|
||||
|
||||
NSError *error;
|
||||
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
|
||||
if (propertyList == nil) {
|
||||
[subscriber sendError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
[subscriber sendNext:propertyList];
|
||||
[subscriber sendCompleted];
|
||||
});
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block {
|
||||
NSParameterAssert(block != nil);
|
||||
|
||||
return [[[self
|
||||
downloadStoreIndexFileLocation]
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [RACSignal create:^(id<RACSubscriber> subscriber) {
|
||||
dispatch_barrier_async(self.queue, ^{
|
||||
if (subscriber.disposable.disposed) return;
|
||||
|
||||
NSError *error;
|
||||
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
|
||||
if (propertyList == nil) {
|
||||
[subscriber sendError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
NSDictionary *newPropertyList = block([propertyList mutableCopy]);
|
||||
if ([newPropertyList isEqual:propertyList]) {
|
||||
[subscriber sendCompleted];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *newData = [NSKeyedArchiver archivedDataWithRootObject:newPropertyList];
|
||||
if (newData == nil) {
|
||||
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
|
||||
[subscriber sendError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListWriteStreamError userInfo:errorInfo]];
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL write = [newData writeToURL:location options:NSDataWritingAtomic error:&error];
|
||||
if (!write) {
|
||||
[subscriber sendError:error];
|
||||
return;
|
||||
}
|
||||
|
||||
[subscriber sendCompleted];
|
||||
});
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
- (NSDictionary *)readPropertyListWithContentsOfURL:(NSURL *)location error:(NSError **)errorRef {
|
||||
NSData *propertyListData = [NSData dataWithContentsOfURL:location options:NSDataReadingUncached error:NULL];
|
||||
if (propertyListData == nil) {
|
||||
return @{};
|
||||
}
|
||||
|
||||
NSDictionary *propertyList = [NSKeyedUnarchiver unarchiveObjectWithData:propertyListData];
|
||||
if (propertyList == nil) {
|
||||
if (errorRef != NULL) {
|
||||
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
|
||||
*errorRef = [NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListReadCorruptError userInfo:errorInfo];
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
return propertyList;
|
||||
}
|
||||
|
||||
+ (NSString *)keyForURL:(NSURL *)URL {
|
||||
return URL.absoluteString;
|
||||
}
|
||||
|
||||
+ (NSString *)fileNameForURL:(NSURL *)URL {
|
||||
NSString *key = [self keyForURL:URL];
|
||||
return [[[key dataUsingEncoding:NSUTF8StringEncoding] sqrl_SHA1Hash] sqrl_base16String];
|
||||
}
|
||||
|
||||
- (RACSignal *)downloadForRequest:(NSURLRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
return [[[[self
|
||||
readDownloadIndex]
|
||||
map:^(NSDictionary *index) {
|
||||
NSString *key = [self.class keyForURL:request.URL];
|
||||
return index[key];
|
||||
}]
|
||||
flattenMap:^(SQRLResumableDownload *download) {
|
||||
if (download != nil) return [RACSignal return:download];
|
||||
|
||||
return [[[self.directoryManager
|
||||
downloadDirectoryURL]
|
||||
map:^(NSURL *downloadDirectory) {
|
||||
return [downloadDirectory URLByAppendingPathComponent:[self.class fileNameForURL:request.URL]];
|
||||
}]
|
||||
map:^(NSURL *location) {
|
||||
return [[SQRLDownload alloc] initWithRequest:request fileURL:location];
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request {
|
||||
NSParameterAssert(download.response != nil);
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
return [[self
|
||||
writeDownloadIndexWithBlock:^(NSMutableDictionary *index) {
|
||||
NSString *key = [self.class keyForURL:request.URL];
|
||||
|
||||
if (download != nil) {
|
||||
index[key] = download;
|
||||
} else {
|
||||
[index removeObjectForKey:key];
|
||||
}
|
||||
|
||||
return index;
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -11,7 +11,7 @@
|
||||
@class SQRLUpdate;
|
||||
|
||||
// A SQRLUpdate that has been successfully downloaded to disk.
|
||||
@interface SQRLDownloadedUpdate : MTLModel
|
||||
@interface SQRLDownloadedUpdate : MTLModel <MTLJSONSerializing>
|
||||
|
||||
// The application bundle representing the downloaded and unarchived update.
|
||||
@property (nonatomic, strong, readonly) NSBundle *bundle;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import "SQRLUpdate.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@interface SQRLDownloadedUpdate ()
|
||||
@@ -40,9 +41,15 @@
|
||||
#pragma mark MTLJSONSerializing
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
|
||||
};
|
||||
return @{};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)updateJSONTransformer {
|
||||
return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:SQRLUpdate.class];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)bundleURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// SQRLInstaller+Private.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLInstaller.h"
|
||||
|
||||
// The defaults key to store a `SQRLInstallerOwnedBundle` so that a moved bundle
|
||||
// can be restored.
|
||||
extern NSString * const SQRLInstallerOwnedBundleKey;
|
||||
|
||||
// The defaults key to store the number of installation attempts that have been
|
||||
// made.
|
||||
extern NSString * const SQRLShipItInstallationAttemptsKey;
|
||||
+19
-23
@@ -11,7 +11,7 @@
|
||||
// The domain for errors originating within SQRLInstaller.
|
||||
extern NSString * const SQRLInstallerErrorDomain;
|
||||
|
||||
// There was an error copying the target or update bundle to a backup location.
|
||||
// There was an error copying the target bundle to the backup location.
|
||||
extern const NSInteger SQRLInstallerErrorBackupFailed;
|
||||
|
||||
// There was an error replacing the target bundle with the update.
|
||||
@@ -34,39 +34,35 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
|
||||
// There was an error moving a bundle across volumes.
|
||||
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
|
||||
|
||||
// There was an error changing the file permissions of the update.
|
||||
extern const NSInteger SQRLInstallerErrorChangingPermissions;
|
||||
@class RACSignal;
|
||||
@class RACAction;
|
||||
@class SQRLShipItState;
|
||||
@class SQRLDirectoryManager;
|
||||
|
||||
@class RACCommand;
|
||||
|
||||
// Performs the installation of an update, saving its intermediate state to user
|
||||
// defaults.
|
||||
// Performs the installation of an update, using the `SQRLShipItState` on disk,
|
||||
// as located by `SQRLDirectoryManager`.
|
||||
//
|
||||
// This class is meant to be used only after the app that will be updated has
|
||||
// terminated.
|
||||
@interface SQRLInstaller : NSObject
|
||||
|
||||
// Initializes an installer using the given application identifier, which is
|
||||
// used to scope resumable state stored to user defaults.
|
||||
// Attempts to install the update or resume an in-progress installation.
|
||||
//
|
||||
// applicationIdentifier - The defaults domain in which to store resumable
|
||||
// state. Must not be nil.
|
||||
- (instancetype)initWithApplicationIdentifier:(NSString *)applicationIdentifier;
|
||||
|
||||
// When executed with a `SQRLShipItRequest`, attempts to install the update or
|
||||
// resume an in-progress installation.
|
||||
//
|
||||
// Each execution will complete or error on an unspecified scheduler when
|
||||
// Returns a signal which completes or errors on an unspecified scheduler when
|
||||
// installation has completed or failed.
|
||||
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
|
||||
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state;
|
||||
|
||||
// When executed with a `SQRLShipItRequest`, aborts an installation, and
|
||||
// attempts to restore the old version of the application if necessary.
|
||||
// Aborts an installation, and attempts to restore the old version of the
|
||||
// application if necessary.
|
||||
//
|
||||
// This must not be executed while `installUpdateCommand` is executing.
|
||||
// This must not be executed while `installUpdateWithState:` is executing.
|
||||
//
|
||||
// Each execution will complete or error on an unspecified scheduler once
|
||||
// Returns a signal while completes or errors on an unspecified scheduler once
|
||||
// aborting/recovery has finished.
|
||||
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
|
||||
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state;
|
||||
|
||||
// Initializes an installer using the given directory manager to read and write the
|
||||
// state of the installation.
|
||||
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
|
||||
|
||||
@end
|
||||
|
||||
+347
-431
@@ -7,24 +7,17 @@
|
||||
//
|
||||
|
||||
#import "SQRLInstaller.h"
|
||||
|
||||
#import <libkern/OSAtomic.h>
|
||||
#import "EXTScope.h"
|
||||
#import <ReactiveCocoa/NSEnumerator+RACSequenceAdditions.h>
|
||||
#import <ReactiveCocoa/NSObject+RACPropertySubscribing.h>
|
||||
#import <ReactiveCocoa/RACCommand.h>
|
||||
#import <ReactiveCocoa/RACSequence.h>
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/RACSubscriber.h>
|
||||
#import <sys/xattr.h>
|
||||
|
||||
#import "NSBundle+SQRLVersionExtensions.h"
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import "RACSignal+SQRLTransactionExtensions.h"
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLTerminationListener.h"
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
#import <libkern/OSAtomic.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <sys/xattr.h>
|
||||
|
||||
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
|
||||
|
||||
@@ -35,58 +28,85 @@ const NSInteger SQRLInstallerErrorInvalidBundleVersion = -4;
|
||||
const NSInteger SQRLInstallerErrorMissingInstallationData = -5;
|
||||
const NSInteger SQRLInstallerErrorInvalidState = -6;
|
||||
const NSInteger SQRLInstallerErrorMovingAcrossVolumes = -7;
|
||||
const NSInteger SQRLInstallerErrorChangingPermissions = -8;
|
||||
|
||||
NSString * const SQRLShipItInstallationAttemptsKey = @"SQRLShipItInstallationAttempts";
|
||||
NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
// Maps an installer state to a selector to invoke.
|
||||
typedef struct {
|
||||
// The state for which the associated method should be invoked.
|
||||
SQRLInstallerState installerState;
|
||||
|
||||
// A method accepting a `SQRLShipItState` argument and returning a cold
|
||||
// signal.
|
||||
//
|
||||
// If NULL, installation should complete.
|
||||
SEL selector;
|
||||
} SQRLInstallerDispatchTableEntry;
|
||||
|
||||
@interface SQRLInstaller ()
|
||||
|
||||
// The defaults domain to store all resumable state in.
|
||||
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
|
||||
// Finds the state file to read and write from.
|
||||
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
|
||||
|
||||
// The bundle currently owned by this installer.
|
||||
// Reads the given key from `state`, failing if it's not set.
|
||||
//
|
||||
// Stores the bundle moved aside by an install request so that the original
|
||||
// bundle can be restored to its original location if needed.
|
||||
@property (atomic, strong) SQRLInstallerOwnedBundle *ownedBundle;
|
||||
|
||||
// Reads the given key from `request`, failing if it's not set.
|
||||
//
|
||||
// key - The property key to read from `request`. This must not be nil, and
|
||||
// should refer to a property of object type.
|
||||
// request - The request object to read. This must not be nil.
|
||||
// key - The property key to read from `state`. This must not be nil, and
|
||||
// should refer to a property of object type.
|
||||
// state - The state object to read. This must not be nil.
|
||||
//
|
||||
// Returns a signal which synchronously sends the non-nil read value then
|
||||
// completes, or errors.
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request;
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state;
|
||||
|
||||
// Moves the updateBundleURL to an owned directory to prevent symlink attack,
|
||||
// takes user:group ownership of the bundle, then verifies that it meets the
|
||||
// designated requirement of the targetBundleURL.
|
||||
// Performs the remaining stages of installation, as specified by `state`.
|
||||
//
|
||||
// request - The request whose update should be prepared and validated.
|
||||
// state - The installation state. This must not be nil.
|
||||
//
|
||||
// Returns a signal which sends the owned & validated bundle URL then completes,
|
||||
// or errors.
|
||||
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request;
|
||||
// Returns a signal which will complete or error on an unspecified thread.
|
||||
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state;
|
||||
|
||||
// Saves a `SQRLInstallerOwnedBundle` for the targetBundleURL to the
|
||||
// preferences, then moves the targetBundleURL to an owned directory.
|
||||
// Moves the specified bundle to a backup location.
|
||||
//
|
||||
// request - The request whose target should be removed in preparation of an
|
||||
// update being installed.
|
||||
// bundleURL - The URL to the bundle that should be backed up. This must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a signal which completes, or errors.
|
||||
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request;
|
||||
// Returns a signal which will send the `NSURL` to the proposed backup location
|
||||
// as soon as possible, then complete once the bundle has actually been moved.
|
||||
- (RACSignal *)backUpBundleAtURL:(NSURL *)bundleURL;
|
||||
|
||||
// Deletes a bundle that was moved into place using -moveAndTakeOwnershipOfBundleAtURL:.
|
||||
// Deletes a bundle that was backed up using -backUpBundleAtURL:.
|
||||
//
|
||||
// bundleURL - The URL to the backup bundle, as sent from -moveAndTakeOwnershipOfBundleAtURL:.
|
||||
// backupURL - The URL to the backup bundle, as sent from -backUpBundleAtURL:.
|
||||
// This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL;
|
||||
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL;
|
||||
|
||||
// Validates the code signature of a bundle, optionally restoring it upon
|
||||
// failure.
|
||||
//
|
||||
// bundleURL - The URL of the bundle whose code signature should be
|
||||
// verified. This must not be nil.
|
||||
// signature - The code signature that the bundle must match. This must
|
||||
// not be nil.
|
||||
// backupBundleURL - If not nil, the URL to a bundle that should replace
|
||||
// `bundleURL` if the code signature does not pass validation.
|
||||
//
|
||||
// Returns a signal which will synchronously complete if `bundleURL` passes
|
||||
// validation, or if the bundle was recovered from `backupBundleURL`. If
|
||||
// validation or recovery fails, an error will be sent.
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL;
|
||||
|
||||
// Attempts to determine whether a bundle has already been moved on disk.
|
||||
//
|
||||
// sourceURL - The original URL to the bundle. This must not be nil.
|
||||
// targetURL - The proposed destination URL for the bundle. This must not be
|
||||
// nil.
|
||||
// signature - The code signature that any item must match in order to be
|
||||
// considered the correct bundle. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send YES if `targetURL` points to
|
||||
// a bundle matching the code signature, NO if it doesn't and `sourceURL` still
|
||||
// exists, or an error otherwise.
|
||||
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature;
|
||||
|
||||
// Moves `sourceURL` to `targetURL`.
|
||||
//
|
||||
@@ -98,7 +118,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
// sourceURL - The URL to move from. This must not be nil.
|
||||
//
|
||||
// Retruns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
|
||||
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
|
||||
|
||||
// Recursively clears the quarantine extended attribute from the given
|
||||
// directory.
|
||||
@@ -112,96 +132,56 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
// Returns a signal which will send completed or error on a background thread.
|
||||
- (RACSignal *)clearQuarantineForDirectory:(NSURL *)directory;
|
||||
|
||||
// Recursively changes the owner and group of the given directory tree to that
|
||||
// of the current process, then disables writing for anyone but the owner.
|
||||
//
|
||||
// directoryURL - The URL to the folder to take ownership of. This must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLInstaller
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithApplicationIdentifier:(NSString *)applicationIdentifier {
|
||||
NSParameterAssert(applicationIdentifier != nil);
|
||||
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
|
||||
NSParameterAssert(directoryManager != nil);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_applicationIdentifier = [applicationIdentifier copy];
|
||||
|
||||
@weakify(self);
|
||||
|
||||
RACSignal *aborting = [[[[RACObserve(self, abortInstallationCommand)
|
||||
ignore:nil]
|
||||
map:^(RACCommand *command) {
|
||||
return command.executing;
|
||||
}]
|
||||
switchToLatest]
|
||||
setNameWithFormat:@"aborting"];
|
||||
|
||||
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItRequest *request) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
// Request can be changed between launches, the installer may have
|
||||
// already have an owned bundle, for a previous targetURL.
|
||||
//
|
||||
// If that's the case, we need to abort the previous owned bundle, and
|
||||
// then handle the new install request.
|
||||
|
||||
return [[[[self
|
||||
abortInstall]
|
||||
doError:^(NSError *error) {
|
||||
NSLog(@"Couldn't abort install and restore owned bundle to previous location %@, error %@", self.ownedBundle.originalURL, error.sqrl_verboseDescription);
|
||||
}]
|
||||
catchTo:[RACSignal empty]]
|
||||
then:^{
|
||||
return [self installRequest:request];
|
||||
}];
|
||||
}];
|
||||
|
||||
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItRequest *request) {
|
||||
@strongify(self);
|
||||
|
||||
return [self abortInstall];
|
||||
}];
|
||||
|
||||
_directoryManager = directoryManager;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Preferences
|
||||
#pragma mark Actions
|
||||
|
||||
- (SQRLInstallerOwnedBundle *)ownedBundle {
|
||||
id archiveData = CFBridgingRelease(CFPreferencesCopyValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost));
|
||||
if (![archiveData isKindOfClass:NSData.class]) return nil;
|
||||
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
SQRLInstallerOwnedBundle *ownedBundle = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
|
||||
if (![ownedBundle isKindOfClass:SQRLInstallerOwnedBundle.class]) return nil;
|
||||
|
||||
return ownedBundle;
|
||||
return [[self
|
||||
resumeInstallationFromState:state]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
|
||||
}
|
||||
|
||||
- (void)setOwnedBundle:(SQRLInstallerOwnedBundle *)ownedBundle {
|
||||
NSData *archiveData = (ownedBundle == nil ? nil : [NSKeyedArchiver archivedDataWithRootObject:ownedBundle]);
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFPropertyListRef)archiveData, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
CFPreferencesSynchronize((__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
|
||||
] reduce:^(NSURL *targetBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [self verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:state.backupBundleURL];
|
||||
}]
|
||||
flatten]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
#pragma mark Installer State
|
||||
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request {
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(key != nil);
|
||||
NSParameterAssert(request != nil);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
id value = [request valueForKey:key];
|
||||
id value = [state valueForKey:key];
|
||||
if (value == nil) {
|
||||
NSString *errorDescription = [NSString stringWithFormat:NSLocalizedString(@"Missing %@", nil), key];
|
||||
return [RACSignal error:[self missingDataErrorWithDescription:errorDescription]];
|
||||
@@ -209,275 +189,309 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
return [RACSignal return:value];
|
||||
}
|
||||
}]
|
||||
setNameWithFormat:@"%@ -getRequiredKey: %@ fromRequest: %@", self, key, request];
|
||||
setNameWithFormat:@"%@ -getRequiredKey: %@ fromState: %@", self, key, state];
|
||||
}
|
||||
|
||||
#pragma mark Installer States
|
||||
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
const SQRLInstallerDispatchTableEntry dispatchTablePrototype[] = {
|
||||
{ .installerState = SQRLInstallerStateNothingToDo, .selector = NULL },
|
||||
{ .installerState = SQRLInstallerStateClearingQuarantine, .selector = @selector(clearQuarantineWithState:) },
|
||||
{ .installerState = SQRLInstallerStateBackingUp, .selector = @selector(backUpWithState:) },
|
||||
{ .installerState = SQRLInstallerStateInstalling, .selector = @selector(installWithState:) },
|
||||
{ .installerState = SQRLInstallerStateVerifyingInPlace, .selector = @selector(verifyInPlaceWithState:) },
|
||||
};
|
||||
|
||||
return [[[[[[[self
|
||||
ownedTemporaryDirectoryURL]
|
||||
flattenMap:^(NSURL *directoryURL) {
|
||||
return [self copyBundleAtURL:request.updateBundleURL toDirectory:directoryURL];
|
||||
}]
|
||||
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
|
||||
NSValue *boxedDispatchTable = [NSValue valueWithBytes:dispatchTablePrototype objCType:@encode(__typeof__(dispatchTablePrototype))];
|
||||
|
||||
RACSignal *step = [RACSignal defer:^{
|
||||
SQRLInstallerDispatchTableEntry dispatchTable[tableCount];
|
||||
[boxedDispatchTable getValue:&dispatchTable];
|
||||
|
||||
SQRLInstallerState installerState = state.installerState;
|
||||
|
||||
size_t tableIndex;
|
||||
for (tableIndex = 0; tableIndex < tableCount; tableIndex++) {
|
||||
if (dispatchTable[tableIndex].installerState == installerState) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tableIndex >= tableCount) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Invalid installer state %i", nil), (int)installerState],
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
|
||||
}
|
||||
|
||||
SEL selector = dispatchTable[tableIndex].selector;
|
||||
if (selector == NULL) {
|
||||
// Nothing to do.
|
||||
return [RACSignal empty];
|
||||
}
|
||||
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
|
||||
invocation.target = self;
|
||||
invocation.selector = selector;
|
||||
|
||||
SQRLShipItState *stateArg = state;
|
||||
[invocation setArgument:&stateArg atIndex:2];
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained RACSignal *step = nil;
|
||||
[invocation getReturnValue:&step];
|
||||
|
||||
SQRLInstallerState nextState;
|
||||
if (tableIndex + 1 >= tableCount) {
|
||||
nextState = SQRLInstallerStateNothingToDo;
|
||||
} else {
|
||||
nextState = dispatchTable[tableIndex + 1].installerState;
|
||||
}
|
||||
|
||||
return [[[step
|
||||
doCompleted:^{
|
||||
NSLog(@"Completed state %i", (int)installerState);
|
||||
}]
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:@(nextState)]];
|
||||
}];
|
||||
|
||||
return [[self
|
||||
stepRepeatedly:step withState:state]
|
||||
setNameWithFormat:@"%@ -resumeInstallationFromState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)stepRepeatedly:(RACSignal *)step withState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(step != nil);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [step flattenMap:^(NSNumber *nextState) {
|
||||
state.installerState = nextState.integerValue;
|
||||
state.installationStateAttempt = 1;
|
||||
return [[state
|
||||
writeUsingURL:self.directoryManager.shipItStateURL]
|
||||
// Automatically begin the next step.
|
||||
concat:[self stepRepeatedly:step withState:state]];
|
||||
}];
|
||||
}
|
||||
|
||||
- (RACSignal *)clearQuarantineWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[self
|
||||
getRequiredKey:@keypath(state.updateBundleURL) fromState:state]
|
||||
flattenMap:^(NSURL *bundleURL) {
|
||||
return [[[self
|
||||
clearQuarantineForDirectory:bundleURL]
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:bundleURL]];
|
||||
return [self clearQuarantineForDirectory:bundleURL];
|
||||
}]
|
||||
zipWith:[self codeSignatureForBundleAtURL:request.targetBundleURL]]
|
||||
reduceEach:^(NSURL *updateBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[[self
|
||||
verifyBundleAtURL:updateBundleURL usingSignature:codeSignature]
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:updateBundleURL]];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -prepareAndValidateUpdateBundleURLForRequest: %@", self, request];
|
||||
setNameWithFormat:@"%@ -clearQuarantineWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
- (RACSignal *)backUpWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
zip:@[
|
||||
[self ownedTemporaryDirectoryURL],
|
||||
[self codeSignatureForBundleAtURL:request.targetBundleURL],
|
||||
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
|
||||
NSURL *targetBundleURL = request.targetBundleURL;
|
||||
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state],
|
||||
] reduce:^(NSURL *bundleURL, SQRLCodeSignature *codeSignature) {
|
||||
RACSignal *skipBackup = [RACSignal return:@NO];
|
||||
if (state.backupBundleURL != nil) {
|
||||
skipBackup = [self checkWhetherBundlePreviouslyAtURL:bundleURL wasInstalledAtURL:state.backupBundleURL usingSignature:codeSignature];
|
||||
}
|
||||
|
||||
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
|
||||
return [skipBackup flattenMap:^(NSNumber *skip) {
|
||||
if (skip.boolValue) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
return [self backUpBundleAtURL:bundleURL];
|
||||
}
|
||||
}];
|
||||
}]
|
||||
doNext:^(SQRLInstallerOwnedBundle *ownedBundle) {
|
||||
self.ownedBundle = ownedBundle;
|
||||
flatten]
|
||||
flattenMap:^(NSURL *backupBundleURL) {
|
||||
// Save the chosen backup URL as soon as we have it, so we
|
||||
// can resume even if the state change hasn't taken effect.
|
||||
//
|
||||
// N.B. It's important that this method remain
|
||||
// synchronous, so it finishes before returning
|
||||
// control to -backUpBundleAtURL:. Really, the flow
|
||||
// here should be refactored so it doesn't matter.
|
||||
state.backupBundleURL = backupBundleURL;
|
||||
return [state writeUsingURL:self.directoryManager.shipItStateURL];
|
||||
}]
|
||||
flattenMap:^(SQRLInstallerOwnedBundle *ownedBundle) {
|
||||
return [self installItemToURL:ownedBundle.temporaryURL fromURL:ownedBundle.originalURL];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request];
|
||||
setNameWithFormat:@"%@ -backUpWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)renameIfNeeded:(SQRLShipItRequest *)request updateBundleURL:(NSURL *)updateBundleURL {
|
||||
if (!request.useUpdateBundleName) return [RACSignal return:request];
|
||||
- (RACSignal *)installWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
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 [[[RACSignal
|
||||
zip:@[
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.updateBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
|
||||
] reduce:^(NSURL *targetBundleURL, NSURL *updateBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[[[self
|
||||
renameIfNeeded:request updateBundleURL:updateBundleURL]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [[self acquireTargetBundleURLForRequest:request] concat:[RACSignal return:request]];
|
||||
checkWhetherBundlePreviouslyAtURL:updateBundleURL wasInstalledAtURL:targetBundleURL usingSignature:codeSignature]
|
||||
flattenMap:^(NSNumber *skip) {
|
||||
if (skip.boolValue) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
return [self installItemAtURL:targetBundleURL fromURL:updateBundleURL];
|
||||
}
|
||||
}]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [[[[[[self
|
||||
installItemToURL:request.targetBundleURL fromURL:updateBundleURL]
|
||||
concat:[RACSignal return:request.updateBundleURL]]
|
||||
concat:[RACSignal return:updateBundleURL]]
|
||||
concat:[RACSignal defer:^{
|
||||
return [RACSignal return:self.ownedBundle.temporaryURL];
|
||||
}]]
|
||||
flattenMap:^(NSURL *location) {
|
||||
return [[[self
|
||||
deleteOwnedBundleAtURL:location]
|
||||
doError:^(NSError *error) {
|
||||
NSLog(@"Couldn't remove owned bundle at location %@, error %@", location, error.sqrl_verboseDescription);
|
||||
}]
|
||||
catchTo:[RACSignal empty]];
|
||||
}]
|
||||
concat:[RACSignal return:request]];
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to replace bundle %@ with update %@", nil), targetBundleURL, updateBundleURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorReplacingTarget toError:error]];
|
||||
}]
|
||||
doCompleted:^{
|
||||
self.ownedBundle = nil;
|
||||
catch:^(NSError *error) {
|
||||
// Verify that the target bundle didn't get corrupted during
|
||||
// failure. Try recovering it if it did.
|
||||
return [[self
|
||||
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
|
||||
// Recovery succeeded, but we still want to pass
|
||||
// through the original error.
|
||||
concat:[RACSignal error:error]];
|
||||
}];
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path]
|
||||
setNameWithFormat:@"%@ -installRequest: %@", self, request];
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -installWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)abortInstall {
|
||||
// The request may have been tampered with to select a new targetURL to
|
||||
// which the moved bundles should be restored.
|
||||
//
|
||||
// Discard the request parameters and restore the owned bundle to its
|
||||
// original location.
|
||||
|
||||
SQRLInstallerOwnedBundle *ownedBundle = self.ownedBundle;
|
||||
if (ownedBundle == nil) return [RACSignal empty];
|
||||
|
||||
return [[[[self
|
||||
installItemToURL:ownedBundle.originalURL fromURL:ownedBundle.temporaryURL]
|
||||
doCompleted:^{
|
||||
self.ownedBundle = nil;
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), ownedBundle.originalURL.path]
|
||||
setNameWithFormat:@"%@ -abortInstall", self];
|
||||
}
|
||||
|
||||
#pragma mark Bundle Ownership
|
||||
|
||||
- (RACSignal *)ownedTemporaryDirectoryURL {
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSString *tmpPath = [NSTemporaryDirectory() stringByResolvingSymlinksInPath];
|
||||
NSString *template = [NSString stringWithFormat:@"%@.XXXXXXXX", self.applicationIdentifier];
|
||||
|
||||
char *fullTemplate = strdup([tmpPath stringByAppendingPathComponent:template].UTF8String);
|
||||
@onExit {
|
||||
free(fullTemplate);
|
||||
};
|
||||
|
||||
if (mkdtemp(fullTemplate) == NULL) {
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
|
||||
}
|
||||
|
||||
NSURL *URL = [NSURL fileURLWithPath:[NSFileManager.defaultManager stringWithFileSystemRepresentation:fullTemplate length:strlen(fullTemplate)] isDirectory:YES];
|
||||
return [RACSignal return:URL];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create temporary folder", nil)];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -ownedDirectoryURL", self];
|
||||
}
|
||||
|
||||
- (RACSignal *)copyBundleAtURL:(NSURL *)bundleURL toDirectory:(NSURL *)directoryURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
NSParameterAssert(directoryURL != nil);
|
||||
|
||||
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:bundleURL.lastPathComponent];
|
||||
- (RACSignal *)verifyInPlaceWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
BOOL copy = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:newBundleURL error:&error];
|
||||
if (!copy) return [RACSignal error:error];
|
||||
|
||||
return [RACSignal return:newBundleURL];
|
||||
zip:@[
|
||||
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
|
||||
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
|
||||
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[self
|
||||
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
|
||||
concat:[[self
|
||||
deleteBackupAtURL:backupBundleURL]
|
||||
catchTo:[RACSignal empty]]];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to copy bundle %@ to directory %@", nil), bundleURL, newBundleURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -copyBundleAtURL: %@ toDirectory: %@", self, bundleURL, directoryURL];
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
#pragma mark Backing Up
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
if ([NSFileManager.defaultManager removeItemAtURL:bundleURL error:&error]) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
- (RACSignal *)backUpBundleAtURL:(NSURL *)targetBundleURL {
|
||||
NSParameterAssert(targetBundleURL != nil);
|
||||
|
||||
return [[[[[[self.directoryManager
|
||||
applicationSupportURL]
|
||||
flattenMap:^(NSURL *applicationSupportURL) {
|
||||
NSError *error = nil;
|
||||
NSURL *temporaryDirectoryURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:applicationSupportURL create:YES error:&error];
|
||||
if (temporaryDirectoryURL == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:temporaryDirectoryURL];
|
||||
}]
|
||||
then:^{
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create backup folder", nil)];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
map:^(NSURL *temporaryDirectoryURL) {
|
||||
return [temporaryDirectoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
|
||||
}]
|
||||
flattenMap:^(NSURL *backupBundleURL) {
|
||||
return [[[[self
|
||||
installItemAtURL:backupBundleURL fromURL:targetBundleURL]
|
||||
catch:^(NSError *error) {
|
||||
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to move bundle %@ to backup location %@", nil), targetBundleURL, backupBundleURL];
|
||||
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
|
||||
}]
|
||||
ignoreValues]
|
||||
// Return the backup URL before doing any work, to increase
|
||||
// fault tolerance.
|
||||
startWith:backupBundleURL];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -backUpBundleAtURL: %@", self, targetBundleURL];
|
||||
}
|
||||
|
||||
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
|
||||
NSParameterAssert(backupURL != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error = nil;
|
||||
if (![NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
// Also remove the temporary directory that the backup lived in.
|
||||
NSURL *temporaryDirectoryURL = bundleURL.URLByDeletingLastPathComponent;
|
||||
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
|
||||
|
||||
// However, use rmdir() to skip it in case there are other files
|
||||
// contained within (for whatever reason).
|
||||
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) == 0) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
int code = errno;
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
|
||||
const char *desc = strerror(code);
|
||||
if (desc != NULL) {
|
||||
userInfo[NSLocalizedDescriptionKey] = @(desc);
|
||||
} else {
|
||||
userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Unknown POSIX error", @"");
|
||||
}
|
||||
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove temp dir \"%@\"", @""), temporaryDirectoryURL.path];
|
||||
userInfo[NSURLErrorKey] = temporaryDirectoryURL;
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]];
|
||||
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) != 0) {
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -deleteOwnedBundleAtURL: %@", self, bundleURL];
|
||||
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
|
||||
}
|
||||
|
||||
#pragma mark Verification
|
||||
|
||||
- (RACSignal *)codeSignatureForBundleAtURL:(NSURL *)URL {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
SQRLCodeSignature *codeSignature = [SQRLCodeSignature signatureWithBundle:URL error:&error];
|
||||
if (codeSignature == nil) return [RACSignal error:error];
|
||||
return [RACSignal return:codeSignature];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -codeSignatureForBundleAtURL: %@", self, URL];
|
||||
}
|
||||
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature {
|
||||
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL {
|
||||
NSParameterAssert(bundleURL != nil);
|
||||
NSParameterAssert(signature != nil);
|
||||
|
||||
return [[[self
|
||||
takeOwnershipOfDirectory:bundleURL]
|
||||
then:^{
|
||||
return [signature verifyBundleAtURL:bundleURL];
|
||||
return [[[signature
|
||||
verifyBundleAtURL:bundleURL]
|
||||
catch:^(NSError *error) {
|
||||
if (backupBundleURL == nil) return [RACSignal error:error];
|
||||
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
|
||||
|
||||
return [self installItemAtURL:bundleURL fromURL:backupBundleURL];
|
||||
}]
|
||||
doCompleted:^{
|
||||
NSLog(@"Restored backup bundle to %@", bundleURL);
|
||||
}]
|
||||
doError:^(NSError *recoveryError) {
|
||||
NSLog(@"Could not restore backup bundle %@ to %@: %@", backupBundleURL, bundleURL, recoveryError.sqrl_verboseDescription);
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@", self, bundleURL, signature];
|
||||
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@ recoveringUsingBackupAtURL: %@", self, bundleURL, signature, backupBundleURL];
|
||||
}
|
||||
|
||||
#pragma mark Installation
|
||||
|
||||
/// Check if the target should be renamed and provide the renamed URL.
|
||||
///
|
||||
/// targetURL - The URL for the target. Cannot be nil.
|
||||
/// sourceURL - The URL for the source. Cannot be nil.
|
||||
///
|
||||
/// Returns a signal which will send the URL for the renamed target. If a rename
|
||||
/// isn't needed then it will send `targetURL`.
|
||||
- (RACSignal *)renamedTargetIfNeededWithTargetURL:(NSURL *)targetURL sourceURL:(NSURL *)sourceURL {
|
||||
return [RACSignal defer:^{
|
||||
NSBundle *sourceBundle = [NSBundle bundleWithURL:sourceURL];
|
||||
NSString *targetExecutableName = targetURL.lastPathComponent.stringByDeletingPathExtension;
|
||||
NSString *sourceExecutableName = sourceBundle.sqrl_executableName;
|
||||
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature {
|
||||
NSParameterAssert(targetURL != nil);
|
||||
NSParameterAssert(sourceURL != nil);
|
||||
NSParameterAssert(signature != nil);
|
||||
|
||||
// If they're already the same then we're good.
|
||||
if ([targetExecutableName isEqual:sourceExecutableName]) {
|
||||
return [RACSignal return:targetURL];
|
||||
}
|
||||
|
||||
NSString *newAppName = [sourceExecutableName stringByAppendingPathExtension:@"app"];
|
||||
NSURL *newTargetURL = [targetURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:newAppName];
|
||||
|
||||
// If there's already something there then don't rename to it.
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:newTargetURL.path]) {
|
||||
return [RACSignal return:targetURL];
|
||||
}
|
||||
|
||||
return [RACSignal return:newTargetURL];
|
||||
}];
|
||||
return [[[[self
|
||||
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
|
||||
concat:[RACSignal return:@YES]]
|
||||
catch:^(NSError *error) {
|
||||
BOOL directory;
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
|
||||
// If the source still exists, this isn't an error.
|
||||
return [RACSignal return:@NO];
|
||||
} else {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}]
|
||||
setNameWithFormat:@"%@ -checkWhetherBundlePreviouslyAtURL: %@ wasInstalledAtURL: %@", self, sourceURL, targetURL];
|
||||
}
|
||||
|
||||
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
|
||||
NSParameterAssert(targetURL != nil);
|
||||
NSParameterAssert(sourceURL != nil);
|
||||
|
||||
@@ -489,7 +503,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
} else {
|
||||
int code = errno;
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
|
||||
|
||||
const char *desc = strerror(code);
|
||||
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
|
||||
|
||||
@@ -530,7 +544,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
return YES;
|
||||
}];
|
||||
|
||||
return enumerator.rac_sequence.signal;
|
||||
return [enumerator.rac_promise start];
|
||||
}]
|
||||
flattenMap:^(NSURL *URL) {
|
||||
const char *path = URL.path.fileSystemRepresentation;
|
||||
@@ -541,16 +555,9 @@ 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);
|
||||
} else {
|
||||
userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Unknown POSIX error", @"");
|
||||
}
|
||||
|
||||
userInfo[NSLocalizedFailureReasonErrorKey] = [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove quarantine attribute from \"%@\". This most likely means the file is read-only.", @""), URL.path];
|
||||
userInfo[NSURLErrorKey] = URL;
|
||||
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
|
||||
}
|
||||
@@ -561,97 +568,6 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
|
||||
setNameWithFormat:@"%@ -clearQuarantineForDirectory: %@", self, directory];
|
||||
}
|
||||
|
||||
#pragma mark File Security
|
||||
|
||||
- (RACSignal *)readFileSecurityOfURL:(NSURL *)location {
|
||||
NSParameterAssert(location != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
NSFileSecurity *fileSecurity;
|
||||
if (![location getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:fileSecurity];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -readFileSecurity: %@", self, location];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeFileSecurity:(NSFileSecurity *)fileSecurity toURL:(NSURL *)location {
|
||||
NSParameterAssert(location != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
if (![location setResourceValue:fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -writeFileSecurity: %@", self, location];
|
||||
}
|
||||
|
||||
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL {
|
||||
NSParameterAssert(directoryURL != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
createSignal:^(id<RACSubscriber> subscriber) {
|
||||
NSDirectoryEnumerator *enumerator = [NSFileManager.defaultManager enumeratorAtURL:directoryURL includingPropertiesForKeys:@[ NSURLFileSecurityKey ] options:0 errorHandler:^ BOOL (NSURL *url, NSError *error) {
|
||||
[subscriber sendError:error];
|
||||
return NO;
|
||||
}];
|
||||
|
||||
return [[enumerator.rac_sequence.signal
|
||||
startWith:directoryURL]
|
||||
subscribe:subscriber];
|
||||
}]
|
||||
flattenMap:^(NSURL *itemURL) {
|
||||
return [[[self
|
||||
readFileSecurityOfURL:itemURL]
|
||||
flattenMap:^(NSFileSecurity *fileSecurity) {
|
||||
if (![self takeOwnershipOfFileSecurity:fileSecurity]) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Permissions Error", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Couldn’t update permissions of %@", nil), itemURL.path],
|
||||
NSURLErrorKey: itemURL
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorChangingPermissions userInfo:errorInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:fileSecurity];
|
||||
}]
|
||||
flattenMap:^(NSFileSecurity *fileSecurity) {
|
||||
return [self writeFileSecurity:fileSecurity toURL:itemURL];
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -takeOwnershipOfDirectory: %@", self, directoryURL];
|
||||
}
|
||||
|
||||
- (BOOL)takeOwnershipOfFileSecurity:(NSFileSecurity *)fileSecurity {
|
||||
CFFileSecurityRef actualFileSecurity = (__bridge CFFileSecurityRef)fileSecurity;
|
||||
|
||||
// If ShipIt is running as root, this will change the owner to
|
||||
// root:wheel.
|
||||
if (!CFFileSecuritySetOwner(actualFileSecurity, getuid())) return NO;
|
||||
if (!CFFileSecuritySetGroup(actualFileSecurity, getgid())) return NO;
|
||||
|
||||
mode_t fileMode = 0;
|
||||
if (!CFFileSecurityGetMode(actualFileSecurity, &fileMode)) return NO;
|
||||
|
||||
// Remove write permission from group and other, leave executable
|
||||
// bit as it was for both.
|
||||
//
|
||||
// Permissions will be r-(x?)r-(x?) afterwards, with owner
|
||||
// permissions left as is.
|
||||
fileMode = (fileMode & ~(S_IWGRP | S_IWOTH));
|
||||
|
||||
return CFFileSecuritySetMode(actualFileSecurity, fileMode);
|
||||
}
|
||||
|
||||
#pragma mark Error Handling
|
||||
|
||||
- (NSError *)missingDataErrorWithDescription:(NSString *)description {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
//
|
||||
// SQRLInstallerOwnedBundle.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
@class SQRLCodeSignature;
|
||||
|
||||
// Tracks original and temporary locations of a bundle. Should be created and
|
||||
// serialised before moving a bundle aside.
|
||||
//
|
||||
// Can be used to ensure new targetURL requests meet the original bundle at that
|
||||
// location's code signature, even though it's been moved aside.
|
||||
@interface SQRLInstallerOwnedBundle : MTLModel
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// originalURL - Where the bundle currently resides, and should be restored to
|
||||
// should an error occur.
|
||||
// temporaryURL - Where the bundle will be moved to, so that another bundle can
|
||||
// take its place at originalURL.
|
||||
// codeSignature - The code signature of the original bundle, so that the
|
||||
// signature can be used irrespective of where the bundle
|
||||
// currently resides.
|
||||
//
|
||||
// Returns an initialised owned bundle for serializing.
|
||||
- (instancetype)initWithOriginalURL:(NSURL *)originalURL temporaryURL:(NSURL *)temporaryURL codeSignature:(SQRLCodeSignature *)codeSignature;
|
||||
|
||||
@property (readonly, copy, nonatomic) NSURL *originalURL;
|
||||
@property (readonly, copy, nonatomic) NSURL *temporaryURL;
|
||||
@property (readonly, copy, nonatomic) SQRLCodeSignature *codeSignature;
|
||||
|
||||
@end
|
||||
@@ -1,23 +0,0 @@
|
||||
//
|
||||
// SQRLInstallerOwnedBundle.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
|
||||
#import "EXTKeyPathCoding.h"
|
||||
|
||||
@implementation SQRLInstallerOwnedBundle
|
||||
|
||||
- (instancetype)initWithOriginalURL:(NSURL *)originalURL temporaryURL:(NSURL *)temporaryURL codeSignature:(SQRLCodeSignature *)codeSignature {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.originalURL): originalURL,
|
||||
@keypath(self.temporaryURL): temporaryURL,
|
||||
@keypath(self.codeSignature): codeSignature,
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// SQRLResumableDownload.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 30/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "SQRLDownload.h"
|
||||
|
||||
// A download which has already been started and already has partial data.
|
||||
@interface SQRLResumableDownload : SQRLDownload
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// request - See `SQRLDownload`.
|
||||
// response - HTTP response whose body is being downloaded to `fileURL`.
|
||||
// Must not be nil.
|
||||
// fileURL - See `SQRLDownload`.
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL;
|
||||
|
||||
// The `response` the receiver was initialised with.
|
||||
@property (readonly, copy, nonatomic) NSHTTPURLResponse *response;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// SQRLResumableDownload.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 30/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLResumableDownload.h"
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
#import "NSHTTPURLResponse+SQRLExtensions.h"
|
||||
|
||||
@implementation SQRLResumableDownload
|
||||
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL {
|
||||
NSParameterAssert(request != nil);
|
||||
NSParameterAssert(response != nil);
|
||||
NSParameterAssert(fileURL != nil);
|
||||
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.request): request,
|
||||
@keypath(self.response): response,
|
||||
@keypath(self.fileURL): fileURL,
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(SQRLResumableDownload *)object {
|
||||
if (self == object) return YES;
|
||||
if (![object isKindOfClass:self.class]) return NO;
|
||||
|
||||
if (!(self.response == nil && object.response == nil) && ![self responseEqual:object.response]) return NO;
|
||||
if (![object.fileURL isEqual:self.fileURL]) return NO;
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)responseEqual:(NSHTTPURLResponse *)response {
|
||||
if (![response.URL isEqual:self.response.URL]) return NO;
|
||||
if (response.statusCode != self.response.statusCode) return NO;
|
||||
if (![response.allHeaderFields isEqual:self.response.allHeaderFields]) return NO;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (RACSignal *)resumableRequest {
|
||||
return [[[super
|
||||
resumableRequest]
|
||||
map:^ NSURLRequest * (NSURLRequest *request) {
|
||||
NSHTTPURLResponse *response = self.response;
|
||||
NSString *ETag = [response sqrl_valueForHTTPHeaderField:@"ETag"];
|
||||
if (ETag == nil) return request;
|
||||
|
||||
NSNumber *alreadyDownloadedSize = nil;
|
||||
BOOL getAlreadyDownloadedSize = [self.fileURL getResourceValue:&alreadyDownloadedSize forKey:NSURLFileSizeKey error:NULL];
|
||||
if (!getAlreadyDownloadedSize) return request;
|
||||
|
||||
NSMutableURLRequest *newRequest = [request mutableCopy];
|
||||
[newRequest setValue:ETag forHTTPHeaderField:@"If-Range"];
|
||||
[newRequest setValue:[NSString stringWithFormat:@"%llu-", alreadyDownloadedSize.unsignedLongLongValue] forHTTPHeaderField:@"Range"];
|
||||
return newRequest;
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -13,7 +13,6 @@
|
||||
#import <Security/Security.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <launch.h>
|
||||
#import "SQRLAuthorization.h"
|
||||
|
||||
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
|
||||
|
||||
@@ -22,7 +21,8 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
@implementation SQRLShipItLauncher
|
||||
|
||||
+ (NSString *)shipItJobLabel {
|
||||
return [SQRLDirectoryManager.currentApplicationManager.applicationIdentifier stringByAppendingString:@".ShipIt"];
|
||||
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
|
||||
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
|
||||
}
|
||||
|
||||
+ (RACSignal *)shipItJobDictionary {
|
||||
@@ -31,13 +31,9 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
|
||||
return [RACSignal zip:@[
|
||||
[directoryManager shipItStdoutURL],
|
||||
[directoryManager shipItStderrURL],
|
||||
[directoryManager shipItStateURL]
|
||||
]];
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL, NSURL *shipItStateURL) {
|
||||
map:^(NSURL *appSupportURL) {
|
||||
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
|
||||
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
|
||||
|
||||
@@ -60,16 +56,14 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
// Pass in the service name so ShipIt knows how to broadcast itself.
|
||||
[arguments addObject:jobLabel];
|
||||
|
||||
// We need to pass the path to ShipIt rather than having ShipIt
|
||||
// calculate the path itself. This is because ShipIt may run as a
|
||||
// different user if it had to escalate to be able to replace the
|
||||
// bundle.
|
||||
[arguments addObject:shipItStateURL.path];
|
||||
|
||||
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = arguments;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = stdoutURL.path;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = stderrURL.path;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
|
||||
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
|
||||
|
||||
#if DEBUG
|
||||
jobDict[@(LAUNCH_JOBKEY_DEBUG)] = @YES;
|
||||
#endif
|
||||
|
||||
return jobDict;
|
||||
}]
|
||||
setNameWithFormat:@"+shipItJobDictionary"];
|
||||
@@ -77,7 +71,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
|
||||
+ (RACSignal *)shipItAuthorization {
|
||||
return [[RACSignal
|
||||
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
AuthorizationItem rightItems[] = {
|
||||
{
|
||||
.name = kSMRightModifySystemDaemons,
|
||||
@@ -114,15 +108,16 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
|
||||
AuthorizationRef authorization = NULL;
|
||||
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
|
||||
|
||||
if (authorizationError == noErr) {
|
||||
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
|
||||
[subscriber sendNext:(__bridge id)authorization];
|
||||
[subscriber sendCompleted];
|
||||
} else {
|
||||
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
|
||||
}
|
||||
|
||||
return nil;
|
||||
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
|
||||
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
|
||||
}]];
|
||||
}]
|
||||
setNameWithFormat:@"+shipItAuthorization"];
|
||||
}
|
||||
@@ -132,13 +127,11 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
zip:@[
|
||||
self.shipItJobDictionary,
|
||||
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
|
||||
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
|
||||
] reduce:^(NSDictionary *jobDictionary, id authorization) {
|
||||
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
|
||||
|
||||
AuthorizationRef authorization = authorizationValue.authorization;
|
||||
|
||||
CFErrorRef cfError;
|
||||
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
|
||||
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
|
||||
NSError *error = CFBridgingRelease(cfError);
|
||||
cfError = NULL;
|
||||
|
||||
@@ -147,7 +140,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
|
||||
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
|
||||
return [RACSignal error:CFBridgingRelease(cfError)];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// SQRLShipItRequest.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// The domain for errors originating within `SQRLShipItRequest`.
|
||||
extern NSString * const SQRLShipItRequestErrorDomain;
|
||||
|
||||
// Errors originating from the `SQRLShipItRequestErrorDomain`.
|
||||
//
|
||||
// SQRLShipItRequestErrorMissingRequiredProperty - A required property was `nil`
|
||||
// upon initialization.
|
||||
//
|
||||
// The `userInfo` dictionary for
|
||||
// this error will contain
|
||||
// `SQRLShipItStatePropertyErrorKey`.
|
||||
//
|
||||
// SQRLShipItRequestErrorUnarchiving - The saved request could not
|
||||
// be unarchived, possibly
|
||||
// because it's invalid.
|
||||
//
|
||||
// SQRLShipItRequestErrorArchiving - The request object could not
|
||||
// be archived.
|
||||
typedef enum : NSInteger {
|
||||
SQRLShipItRequestErrorMissingRequiredProperty = 1,
|
||||
SQRLShipItRequestErrorUnarchiving = 2,
|
||||
SQRLShipItRequestErrorArchiving = 3,
|
||||
} SQRLShipItRequestError;
|
||||
|
||||
// Associated with an `NSString` indicating the required property key that did
|
||||
// not have a value upon initialization.
|
||||
extern NSString * const SQRLShipItRequestPropertyErrorKey;
|
||||
|
||||
// Constructed and written to disk for `ShipIt` to pick up. This represents a
|
||||
// single update request from the client's perspective.
|
||||
@interface SQRLShipItRequest : MTLModel
|
||||
|
||||
// Reads a `SQRLShipItState` from disk, at the location specified by the given
|
||||
// URL signal.
|
||||
//
|
||||
// URLSignal - Determines the file location to read from, the signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
|
||||
// complete, or error.
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URLSignal;
|
||||
|
||||
// Writes the receiver to disk, at the location specified by the given URL
|
||||
// signal.
|
||||
//
|
||||
// URLSignal - Determines the file location to write to. The signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URLSignal;
|
||||
|
||||
// Designated initialiser.
|
||||
//
|
||||
// updateBundleURL - The update bundle which will replace
|
||||
// targetBundleURL. Must not be nil.
|
||||
// targetBundleURL - Where the update should be installed, if a bundle
|
||||
// is already present, the update is checked for
|
||||
// suitability against this bundle. Must not be nil.
|
||||
// bundleIdentifier - The bundle identifier that the installer should
|
||||
// wait for instances of to terminate before
|
||||
// installing. Can be nil.
|
||||
// launchAfterInstallation - Whether the updated application should be launched
|
||||
// after installation.
|
||||
// useUpdateBundleName - Should the target use the update bundle's name?
|
||||
//
|
||||
// Returns a request which can be written to disk for ShipIt to read and
|
||||
// perform.
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation useUpdateBundleName:(BOOL)useUpdateBundleName;
|
||||
|
||||
// The URL to the downloaded update's app bundle.
|
||||
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
|
||||
|
||||
// The URL to the app bundle that should be replaced with the update.
|
||||
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
|
||||
|
||||
// The bundle identifier of the application being updated.
|
||||
//
|
||||
// If not nil, the installer will wait for applications matching this identifier
|
||||
// (and `targetBundleURL`) to terminate before continuing.
|
||||
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
|
||||
|
||||
// Whether to launch the application after an update is successfully installed.
|
||||
@property (nonatomic, assign, readonly) BOOL launchAfterInstallation;
|
||||
|
||||
// Whether the app should use the update bundle's name.
|
||||
@property (nonatomic, assign, readonly) BOOL useUpdateBundleName;
|
||||
|
||||
@end
|
||||
@@ -1,194 +0,0 @@
|
||||
//
|
||||
// SQRLShipItRequest.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 08/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
#import "EXTKeyPathCoding.h"
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
|
||||
NSString * const SQRLShipItRequestErrorDomain = @"SQRLShipItRequestErrorDomain";
|
||||
|
||||
NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestPropertyErrorKey";
|
||||
|
||||
@interface SQRLShipItRequest () <MTLJSONSerializing>
|
||||
@end
|
||||
|
||||
@implementation SQRLShipItRequest
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionary error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
|
||||
if ([self valueForKey:key] != nil) return YES;
|
||||
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorMissingRequiredProperty userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
};
|
||||
|
||||
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
|
||||
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation useUpdateBundleName:(BOOL)useUpdateBundleName {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.updateBundleURL): updateBundleURL,
|
||||
@keypath(self.targetBundleURL): targetBundleURL,
|
||||
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
|
||||
@keypath(self.launchAfterInstallation): @(launchAfterInstallation),
|
||||
@keypath(self.useUpdateBundleName): @(useUpdateBundleName),
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)updateBundleURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)targetBundleURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[[URL
|
||||
flattenMap:^(NSURL *stateURL) {
|
||||
NSError *error;
|
||||
NSData *data = [self readFromURL:stateURL error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}]
|
||||
flattenMap:^(NSData *data) {
|
||||
return [self readFromData:data];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read update request", nil),
|
||||
};
|
||||
if (error != nil) {
|
||||
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
NSUnderlyingErrorKey: error,
|
||||
}];
|
||||
}
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorUnarchiving userInfo:userInfo]];
|
||||
}]
|
||||
setNameWithFormat:@"+readUsingURL: %@", URL];
|
||||
}
|
||||
|
||||
+ (NSData *)readFromURL:(NSURL *)URL error:(NSError **)errorRef {
|
||||
__block NSData *data = nil;
|
||||
|
||||
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
|
||||
[coordinator coordinateReadingItemAtURL:URL options:NSFileCoordinatorReadingWithoutChanges error:errorRef byAccessor:^(NSURL *newURL) {
|
||||
data = [NSData dataWithContentsOfURL:newURL options:NSDataReadingUncached error:errorRef];
|
||||
}];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (RACSignal *)readFromData:(NSData *)data {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error;
|
||||
NSDictionary *JSONDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
||||
if (JSONDictionary == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
SQRLShipItRequest *request = [MTLJSONAdapter modelOfClass:SQRLShipItRequest.class fromJSONDictionary:JSONDictionary error:&error];
|
||||
if (request == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:request];
|
||||
|
||||
}]
|
||||
setNameWithFormat:@"+readFromData: <NSData %p>", data];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
zip:@[
|
||||
URL,
|
||||
[self serialization]
|
||||
] reduce:^(NSURL *stateURL, NSData *data) {
|
||||
NSError *error;
|
||||
BOOL write = [self writeData:data toURL:stateURL error:&error];
|
||||
if (!write) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flatten]
|
||||
catch:^(NSError *error) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not write update request", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil),
|
||||
};
|
||||
if (error != nil) {
|
||||
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
NSUnderlyingErrorKey: error,
|
||||
}];
|
||||
}
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorArchiving userInfo:userInfo]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
|
||||
}
|
||||
|
||||
- (BOOL)writeData:(NSData *)data toURL:(NSURL *)URL error:(NSError **)errorRef {
|
||||
__block BOOL success = NO;
|
||||
|
||||
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
|
||||
[coordinator coordinateWritingItemAtURL:URL options:0 error:errorRef byAccessor:^(NSURL *newURL) {
|
||||
success = [data writeToURL:newURL options:NSDataWritingAtomic error:errorRef];
|
||||
}];
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
- (RACSignal *)serialization {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:self];
|
||||
|
||||
NSError *error;
|
||||
NSData *data = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}]
|
||||
setNameWithFormat:@"%@ serialization", self];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// SQRLShipItState.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-08.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
// The domain for errors originating within `SQRLShipItState`.
|
||||
extern NSString * const SQRLShipItStateErrorDomain;
|
||||
|
||||
// A required property was `nil` upon initialization.
|
||||
//
|
||||
// The `userInfo` dictionary for this error will contain
|
||||
// `SQRLShipItStatePropertyErrorKey`.
|
||||
extern const NSInteger SQRLShipItStateErrorMissingRequiredProperty;
|
||||
|
||||
// The saved state on disk could not be unarchived, possibly because it's
|
||||
// invalid.
|
||||
extern const NSInteger SQRLShipItStateErrorUnarchiving;
|
||||
|
||||
// The state object could not be archived.
|
||||
extern const NSInteger SQRLShipItStateErrorArchiving;
|
||||
|
||||
// Associated with an NSString indicating the required property key that did not
|
||||
// have a value upon initialization.
|
||||
extern NSString * const SQRLShipItStatePropertyErrorKey;
|
||||
|
||||
// The current state of the installer, for persistence across relaunches and for
|
||||
// tolerance of system failures.
|
||||
//
|
||||
// SQRLInstallerStateNothingToDo - ShipIt has not started installing yet.
|
||||
// SQRLInstallerStateClearingQuarantine - Clearing the quarantine flag on the
|
||||
// update bundle so it can used without
|
||||
// issue.
|
||||
// SQRLInstallerStateBackingUp - Backing up the target bundle so it can
|
||||
// be restored in the event of failure.
|
||||
// SQRLInstallerStateInstalling - Replacing the target bundle with the
|
||||
// update bundle.
|
||||
// SQRLInstallerStateVerifyingInPlace - Verifying that the target bundle is
|
||||
// still valid after updating.
|
||||
//
|
||||
// Note that these values must remain backwards compatible, so ShipIt doesn't
|
||||
// start up in a weird mode on a newer version.
|
||||
typedef enum : NSInteger {
|
||||
SQRLInstallerStateNothingToDo = 0,
|
||||
SQRLInstallerStateClearingQuarantine,
|
||||
SQRLInstallerStateBackingUp,
|
||||
SQRLInstallerStateInstalling,
|
||||
SQRLInstallerStateVerifyingInPlace,
|
||||
} SQRLInstallerState;
|
||||
|
||||
@class RACSignal;
|
||||
@class SQRLCodeSignature;
|
||||
|
||||
// Encapsulates all the state needed by the ShipIt process.
|
||||
@interface SQRLShipItState : MTLModel
|
||||
|
||||
// Reads a `SQRLShipItState` from disk, at the location specified by the given
|
||||
// URL signal.
|
||||
//
|
||||
// URLSignal - Determines the file location to read from, the signal should send
|
||||
// an `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously send a `SQRLShipItState` then
|
||||
// complete, or error.
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL;
|
||||
|
||||
// Initializes the receiver with the arguments that will not change during
|
||||
// installation.
|
||||
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature;
|
||||
|
||||
// Writes the receiver to disk, at the location specified by the given URL
|
||||
// signal.
|
||||
//
|
||||
// URL - Determines the file location to write to. The signal should send an
|
||||
// `NSURL` object then complete, or error. This must not be nil.
|
||||
//
|
||||
// Returns a signal which will synchronously complete or error.
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL;
|
||||
|
||||
// The URL to the app bundle that should be replaced with an update.
|
||||
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
|
||||
|
||||
// The URL to the downloaded update's app bundle.
|
||||
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
|
||||
|
||||
// A code signature that the update bundle must match in order to be valid.
|
||||
@property (nonatomic, copy, readonly) SQRLCodeSignature *codeSignature;
|
||||
|
||||
// The bundle identifier of the application being updated.
|
||||
//
|
||||
// If not nil, the installer will wait for applications matching this identifier
|
||||
// (and `targetBundleURL`) to terminate before continuing.
|
||||
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
|
||||
|
||||
// The current state of the installer.
|
||||
@property (atomic, assign) SQRLInstallerState installerState;
|
||||
|
||||
// The number of installation attempts that have occurred for the current
|
||||
// `state`.
|
||||
@property (atomic, assign) NSUInteger installationStateAttempt;
|
||||
|
||||
// Whether to relaunch the application after an update is successfully
|
||||
// installed.
|
||||
@property (atomic, assign) BOOL relaunchAfterInstallation;
|
||||
|
||||
// The URL where the target bundle has been backed up to before installing the
|
||||
// update.
|
||||
//
|
||||
// This property is set automatically during the course of installation. It
|
||||
// should not be preset.
|
||||
@property (atomic, copy) NSURL *backupBundleURL;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,122 @@
|
||||
//
|
||||
// SQRLShipItState.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-08.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLShipItState.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
NSString * const SQRLShipItStateErrorDomain = @"SQRLShipItStateErrorDomain";
|
||||
NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErrorKey";
|
||||
|
||||
const NSInteger SQRLShipItStateErrorMissingRequiredProperty = 1;
|
||||
const NSInteger SQRLShipItStateErrorUnarchiving = 2;
|
||||
const NSInteger SQRLShipItStateErrorArchiving = 3;
|
||||
|
||||
@implementation SQRLShipItState
|
||||
|
||||
#pragma mark Lifecycle
|
||||
|
||||
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
|
||||
self = [super initWithDictionary:dictionary error:error];
|
||||
if (self == nil) return nil;
|
||||
|
||||
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
|
||||
if ([self valueForKey:key] != nil) return YES;
|
||||
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
};
|
||||
|
||||
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
|
||||
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
|
||||
if (!validateKey(@keypath(self.codeSignature))) return nil;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature {
|
||||
return [self initWithDictionary:@{
|
||||
@keypath(self.targetBundleURL): targetBundleURL,
|
||||
@keypath(self.updateBundleURL): updateBundleURL,
|
||||
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
|
||||
@keypath(self.codeSignature): codeSignature,
|
||||
} error:NULL];
|
||||
}
|
||||
|
||||
#pragma mark Serialization
|
||||
|
||||
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
return [[[URL
|
||||
flattenMap:^(NSURL *stateURL) {
|
||||
NSError *error = nil;
|
||||
NSData *data = [NSData dataWithContentsOfURL:stateURL options:NSDataReadingUncached error:&error];
|
||||
if (data == nil) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}]
|
||||
flattenMap:^(NSData *data) {
|
||||
SQRLShipItState *state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
|
||||
if (![state isKindOfClass:SQRLShipItState.class]) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read saved state", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:state];
|
||||
}]
|
||||
setNameWithFormat:@"+readUsingURL: %@", URL];
|
||||
}
|
||||
|
||||
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
|
||||
NSParameterAssert(URL != nil);
|
||||
|
||||
RACSignal *serialization = [RACSignal defer:^{
|
||||
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self];
|
||||
if (data == nil) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not save state", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
}];
|
||||
|
||||
return [[[RACSignal
|
||||
zip:@[
|
||||
URL,
|
||||
serialization
|
||||
] reduce:^(NSURL *stateURL, NSData *data) {
|
||||
NSError *error = nil;
|
||||
if (![data writeToURL:stateURL options:NSDataWritingAtomic error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,7 +6,7 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
|
||||
@@ -7,14 +7,7 @@
|
||||
//
|
||||
|
||||
#import "SQRLTerminationListener.h"
|
||||
|
||||
#import "EXTKeyPathCoding.h"
|
||||
#import <ReactiveCocoa/NSArray+RACSequenceAdditions.h>
|
||||
#import <ReactiveCocoa/RACDisposable.h>
|
||||
#import <ReactiveCocoa/RACScheduler.h>
|
||||
#import <ReactiveCocoa/RACSequence.h>
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/RACSubscriber.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@interface SQRLTerminationListener ()
|
||||
|
||||
@@ -52,7 +45,7 @@
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
|
||||
return [apps.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
|
||||
return apps.rac_signal;
|
||||
}]
|
||||
filter:^(NSRunningApplication *application) {
|
||||
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
|
||||
@@ -67,7 +60,7 @@
|
||||
|
||||
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
|
||||
return [[RACSignal
|
||||
createSignal:^(id<RACSubscriber> subscriber) {
|
||||
create:^(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, ^{
|
||||
@@ -79,9 +72,10 @@
|
||||
});
|
||||
|
||||
dispatch_resume(source);
|
||||
return [RACDisposable disposableWithBlock:^{
|
||||
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
|
||||
dispatch_source_cancel(source);
|
||||
}];
|
||||
dispatch_release(source);
|
||||
}]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// SQRLURLConnection.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 25/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class SQRLDownloadManager;
|
||||
@class RACSignal;
|
||||
|
||||
// Resumably download a remote resource.
|
||||
//
|
||||
// If the resource has previously been downloaded this operation may return the
|
||||
// URL of an existing file, contingent on the server matching the ETag of the
|
||||
// original response.
|
||||
@interface SQRLURLConnection : NSObject
|
||||
|
||||
// This is the designated initialiser. The body of the response is streamed to
|
||||
// disk. If the response includes an ETag the download can be resumed
|
||||
// automatically by subsequent download operations initialised with a request of
|
||||
// equal URL.
|
||||
//
|
||||
// request - Must not be nil, the request is issued with NSURLConnection.
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request __attribute__((nonnull (1)));
|
||||
|
||||
// Starts accumulating the resource body in memory, cancels when the
|
||||
// subscription is disposed.
|
||||
//
|
||||
// Returns a signal when sends a tuple of `NSURLResponse` and `NSData`, then
|
||||
// completes or errors.
|
||||
- (RACSignal *)retrieve;
|
||||
|
||||
// Starts downloading the resource body to disk, resuming from a previous
|
||||
// download if possible, cancels when the subscription is disposed.
|
||||
//
|
||||
// downloadManager - Must be non nil, determines where the downloads will be
|
||||
// stored and resumed from. For a previously started download
|
||||
// to be resumed, an equivalent download manager should be
|
||||
// provided.
|
||||
//
|
||||
// Returns a signal which sends a tuple of `NSURLResponse` and a file
|
||||
// scheme `NSURL` where the resource has been stored on disk, then completes, or
|
||||
// errors.
|
||||
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager __attribute__((nonnull (1)));
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,257 @@
|
||||
//
|
||||
// SQRLURLConnection.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 25/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLURLConnection.h"
|
||||
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
|
||||
#import "SQRLDownloadManager.h"
|
||||
#import "SQRLResumableDownload.h"
|
||||
|
||||
@interface SQRLURLConnection ()
|
||||
// The request the downloader was initialised with.
|
||||
@property (nonatomic, copy, readonly) NSURLRequest *request;
|
||||
|
||||
// Listens for invocations of `selector` on the receiver.
|
||||
//
|
||||
// The first argument of `selector` must be an `NSURLConnection` object.
|
||||
//
|
||||
// Returns a signal which sends the _second_ argument of `selector`, or
|
||||
// RACUnit if there is only one argument, then completes when the receiver
|
||||
// deallocates.
|
||||
- (RACSignal *)signalForDelegateSelector:(SEL)selector;
|
||||
|
||||
// Subscribes to the connection delegate selectors and starts a connection for
|
||||
// `request`.
|
||||
//
|
||||
// request - The request to perform, must not be nil.
|
||||
// mapData - For each response, map the data received for that response to a
|
||||
// result.
|
||||
//
|
||||
// Returns a signal which sends a tuple of the last response, and the first
|
||||
// value from the last `mapData`, then completes, or errors.
|
||||
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLURLConnection
|
||||
|
||||
- (instancetype)initWithRequest:(NSURLRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
self = [self init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_request = [request copy];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (RACSignal *)signalForDelegateSelector:(SEL)selector {
|
||||
NSParameterAssert(selector != NULL);
|
||||
|
||||
return [[self
|
||||
rac_signalForSelector:selector]
|
||||
map:^(RACTuple *args) {
|
||||
return args[1] ?: RACUnit.defaultUnit;
|
||||
}];
|
||||
}
|
||||
|
||||
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData {
|
||||
return [[RACSignal
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
// A signal that will error if the connection fails for any reason.
|
||||
RACSignal *errors = [[self
|
||||
signalForDelegateSelector:@selector(connection:didFailWithError:)]
|
||||
flattenMap:^(NSError *error) {
|
||||
return [RACSignal error:error];
|
||||
}];
|
||||
|
||||
// Sends (or replays) RACUnit when the connection has finished
|
||||
// loading successfully.
|
||||
RACSignal *finished = [[[[self
|
||||
signalForDelegateSelector:@selector(connectionDidFinishLoading:)]
|
||||
take:1]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
start];
|
||||
|
||||
// A signal of all `NSURLResponse`s received on the connection.
|
||||
RACSignal *responses = [[self
|
||||
signalForDelegateSelector:@selector(connection:didReceiveResponse:)]
|
||||
takeUntil:finished];
|
||||
|
||||
// A signal of all `NSData` received on the connection.
|
||||
RACSignal *data = [[self
|
||||
signalForDelegateSelector:@selector(connection:didReceiveData:)]
|
||||
takeUntil:finished];
|
||||
|
||||
[[[[[RACSignal
|
||||
merge:@[ responses, errors ]]
|
||||
takeUntil:finished]
|
||||
map:^(NSURLResponse *response) {
|
||||
return [RACSignal
|
||||
zip:@[
|
||||
[RACSignal return:response],
|
||||
mapData(response, data),
|
||||
]];
|
||||
}]
|
||||
switchToLatest]
|
||||
subscribe:subscriber];
|
||||
|
||||
NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
|
||||
delegateQueue.maxConcurrentOperationCount = 1;
|
||||
|
||||
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
|
||||
connection.delegateQueue = delegateQueue;
|
||||
|
||||
[connection start];
|
||||
|
||||
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
|
||||
[connection cancel];
|
||||
}]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), request];
|
||||
}
|
||||
|
||||
#pragma mark Download
|
||||
|
||||
- (RACSignal *)truncateDownload:(SQRLDownload *)download {
|
||||
NSParameterAssert(download != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSError *error = nil;
|
||||
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:download.fileURL error:&error];
|
||||
return (remove ? [RACSignal empty] : [RACSignal error:error]);
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
|
||||
return [RACSignal error:error];
|
||||
}]
|
||||
concat:[RACSignal return:download]]
|
||||
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
|
||||
}
|
||||
|
||||
- (RACSignal *)recordDownload:(SQRLResumableDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager {
|
||||
NSParameterAssert(download != nil);
|
||||
NSParameterAssert(downloadManager != nil);
|
||||
|
||||
return [[[downloadManager
|
||||
setDownload:download forRequest:self.request]
|
||||
concat:[RACSignal return:download]]
|
||||
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
|
||||
}
|
||||
|
||||
- (RACSignal *)prepareDownload:(SQRLDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager forResponse:(NSURLResponse *)response {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
if (![response isKindOfClass:NSHTTPURLResponse.class]) {
|
||||
return [self truncateDownload:download];
|
||||
}
|
||||
|
||||
NSHTTPURLResponse *httpResponse = (id)response;
|
||||
|
||||
RACSignal *downloadSignal;
|
||||
if (httpResponse.statusCode != 206 /* Partial Data */) {
|
||||
downloadSignal = [self truncateDownload:download];
|
||||
} else {
|
||||
downloadSignal = [RACSignal empty];
|
||||
}
|
||||
|
||||
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:self.request response:httpResponse fileURL:download.fileURL];
|
||||
|
||||
return [[downloadSignal
|
||||
ignoreValues]
|
||||
concat:[self recordDownload:newDownload downloadManager:downloadManager]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), response];
|
||||
}
|
||||
|
||||
- (BOOL)appendData:(NSData *)data toURL:(NSURL *)fileURL error:(NSError **)errorRef {
|
||||
NSOutputStream *outputStream = [NSOutputStream outputStreamWithURL:fileURL append:YES];
|
||||
|
||||
[outputStream open];
|
||||
@onExit {
|
||||
[outputStream close];
|
||||
};
|
||||
|
||||
uint8_t const *bytes = data.bytes;
|
||||
size_t length = data.length;
|
||||
while (length > 0) {
|
||||
NSInteger written = [outputStream write:bytes maxLength:length];
|
||||
if (written == -1) {
|
||||
NSError *streamError = outputStream.streamError;
|
||||
if ([streamError.domain isEqualToString:NSPOSIXErrorDomain] && streamError.code == EINTR) continue;
|
||||
|
||||
if (errorRef != NULL) *errorRef = streamError;
|
||||
return NO;
|
||||
}
|
||||
|
||||
bytes += written;
|
||||
length -= written;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark Start
|
||||
|
||||
- (RACSignal *)retrieve {
|
||||
return [[self
|
||||
connectionSignalWithRequest:self.request mapData:^(id _, RACSignal *data) {
|
||||
return [data
|
||||
aggregateWithStart:[NSMutableData data] reduce:^(NSMutableData *running, NSData *data) {
|
||||
[running appendData:data];
|
||||
return running;
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
|
||||
}
|
||||
|
||||
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager {
|
||||
NSParameterAssert(downloadManager != nil);
|
||||
|
||||
RACSignal *download = [[[downloadManager
|
||||
downloadForRequest:self.request]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
deferred];
|
||||
|
||||
return [[[download
|
||||
flattenMap:^(SQRLDownload *download) {
|
||||
return [download resumableRequest];
|
||||
}]
|
||||
flattenMap:^(NSURLRequest *request) {
|
||||
return [self connectionSignalWithRequest:request mapData:^(NSURLResponse *response, RACSignal *data) {
|
||||
RACSignal *downloadURL = [[[[download
|
||||
flattenMap:^(SQRLDownload *download) {
|
||||
return [self prepareDownload:download downloadManager:downloadManager forResponse:response];
|
||||
}]
|
||||
map:^(SQRLDownload *download) {
|
||||
return download.fileURL;
|
||||
}]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
deferred];
|
||||
|
||||
return [[[[data
|
||||
map:^(NSData *bodyData) {
|
||||
return [[downloadURL
|
||||
try:^(NSURL *fileURL, NSError **errorRef) {
|
||||
return [self appendData:bodyData toURL:fileURL error:errorRef];
|
||||
}]
|
||||
ignoreValues];
|
||||
}]
|
||||
concat]
|
||||
ignoreValues]
|
||||
concat:downloadURL];
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ download: %@", self, downloadManager];
|
||||
}
|
||||
|
||||
@end
|
||||
+11
-9
@@ -14,6 +14,8 @@ NSString * const SQRLUpdateJSONReleaseNotesKey = @"notes";
|
||||
NSString * const SQRLUpdateJSONNameKey = @"name";
|
||||
NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
|
||||
|
||||
NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
|
||||
|
||||
@implementation SQRLUpdate
|
||||
|
||||
#pragma mark Lifecycle
|
||||
@@ -50,7 +52,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
|
||||
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
|
||||
}
|
||||
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -61,10 +63,10 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
|
||||
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
|
||||
return @{
|
||||
@keypath(SQRLUpdate.new, releaseNotes): @"notes",
|
||||
@keypath(SQRLUpdate.new, releaseName): @"name",
|
||||
@keypath(SQRLUpdate.new, releaseDate): @"pub_date",
|
||||
@keypath(SQRLUpdate.new, updateURL): @"url",
|
||||
@keypath(SQRLUpdate.new, releaseNotes): SQRLUpdateJSONReleaseNotesKey,
|
||||
@keypath(SQRLUpdate.new, releaseName): SQRLUpdateJSONNameKey,
|
||||
@keypath(SQRLUpdate.new, releaseDate): SQRLUpdateJSONPublicationDateKey,
|
||||
@keypath(SQRLUpdate.new, updateURL): SQRLUpdateJSONURLKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,7 +76,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;
|
||||
@@ -94,8 +96,8 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
|
||||
}
|
||||
|
||||
// If neither match, try removing the ':' in the time zone
|
||||
static NSRegularExpression *timeZoneSuffix = nil;
|
||||
static dispatch_once_t timeZoneSuffixPredicate = 0;
|
||||
static NSRegularExpression *timeZoneSuffix;
|
||||
static dispatch_once_t timeZoneSuffixPredicate;
|
||||
dispatch_once(&timeZoneSuffixPredicate, ^ {
|
||||
timeZoneSuffix = [NSRegularExpression regularExpressionWithPattern:@"([-+])([0-9]{2}):([0-9]{2})$" options:0 error:NULL];
|
||||
});
|
||||
@@ -142,7 +144,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
|
||||
};
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
|
||||
}
|
||||
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
+5
-53
@@ -6,26 +6,6 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
// Represents the current state of the updater.
|
||||
//
|
||||
// SQRLUpdaterStateIdle - Doing absolutely diddly squat.
|
||||
// SQRLUpdaterStateCheckingForUpdate - Checking for any updates from the server.
|
||||
// SQRLUpdaterStateDownloadingUpdate - Update found, downloading the archive.
|
||||
// SQRLUpdaterStateAwaitingRelaunch - Awaiting a relaunch to install
|
||||
// the update.
|
||||
typedef enum : NSUInteger {
|
||||
SQRLUpdaterStateIdle,
|
||||
SQRLUpdaterStateCheckingForUpdate,
|
||||
SQRLUpdaterStateDownloadingUpdate,
|
||||
SQRLUpdaterStateAwaitingRelaunch,
|
||||
} SQRLUpdaterState;
|
||||
|
||||
// Block for providing download requests given a download url
|
||||
typedef NSURLRequest * (^SQRLRequestForDownload)(NSURL *);
|
||||
|
||||
// The domain for errors originating within SQRLUpdater.
|
||||
extern NSString * const SQRLUpdaterErrorDomain;
|
||||
|
||||
@@ -63,7 +43,7 @@ extern NSString * const SQRLUpdaterServerDataErrorKey;
|
||||
// error with code `SQRLUpdaterErrorInvalidJSON` is generated.
|
||||
extern NSString * const SQRLUpdaterJSONObjectErrorKey;
|
||||
|
||||
@class RACCommand;
|
||||
@class RACAction;
|
||||
@class RACDisposable;
|
||||
@class RACSignal;
|
||||
|
||||
@@ -72,19 +52,12 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
|
||||
|
||||
// Kicks off a check for updates.
|
||||
//
|
||||
// If an update is available, it will be sent on `updates` once downloaded.
|
||||
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
|
||||
|
||||
// The current state of the manager.
|
||||
//
|
||||
// This property is KVO-compliant.
|
||||
@property (atomic, readonly) SQRLUpdaterState state;
|
||||
// If an update is available, it will be sent on
|
||||
// `[checkForUpdatesAction deferred]` and `updates` once downloaded.
|
||||
@property (nonatomic, strong, readonly) RACAction *checkForUpdatesAction;
|
||||
|
||||
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
|
||||
// update is available.
|
||||
//
|
||||
// This signal is actually just `checkForUpdatesCommand.executionSignals`,
|
||||
// flattened for convenience.
|
||||
// update is available. Completes when the receiver deallocates.
|
||||
@property (nonatomic, strong, readonly) RACSignal *updates;
|
||||
|
||||
// The request that will be sent to check for updates.
|
||||
@@ -95,15 +68,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
|
||||
// This property must never be set to nil.
|
||||
@property (atomic, copy) NSURLRequest *updateRequest;
|
||||
|
||||
// The block used for fetching a given download request
|
||||
//
|
||||
// The default value is the argument that was originally passed to
|
||||
// -initWithUpdateRequest:requestForDownload:.
|
||||
//
|
||||
// If initialized with -initWithUpdateRequest: this block will
|
||||
// return a generic NSURLRequest with the provided url.
|
||||
@property (nonatomic, copy) SQRLRequestForDownload requestForDownload;
|
||||
|
||||
// The `SQRLUpdate` subclass to instantiate with the server's response.
|
||||
//
|
||||
// By default, this is `SQRLUpdate` itself, but it can be set to a custom
|
||||
@@ -123,18 +87,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
|
||||
// Returns the initialized `SQRLUpdater`.
|
||||
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest;
|
||||
|
||||
// Initializes an updater that will send the given request to check for updates
|
||||
// and passes a block to provide requests for the update downloads.
|
||||
//
|
||||
// updateRequest - Same as with initWithUpdateRequest
|
||||
// requestForDownload - Once the update url is found for the update download, allow
|
||||
// providing custom requests that can be costomized as desired.
|
||||
// Useful for including `Authorization` headers just like the
|
||||
// updateRequest param.
|
||||
//
|
||||
// Returns the initialized `SQRLUpdater`.
|
||||
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload;
|
||||
|
||||
// Executes `checkForUpdatesCommand` (if enabled) every `interval` seconds.
|
||||
//
|
||||
// The first check will not occur until `interval` seconds have passed.
|
||||
|
||||
+222
-367
@@ -15,12 +15,15 @@
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLUpdate.h"
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <sys/mount.h>
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLDownloadManager.h"
|
||||
|
||||
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
|
||||
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
|
||||
@@ -33,20 +36,10 @@ const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
|
||||
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
|
||||
|
||||
/// The application's being run on a read-only volume.
|
||||
const NSInteger SQRLUpdaterErrorReadOnlyVolume = 8;
|
||||
|
||||
// The prefix used when creating temporary directories for updates. This will be
|
||||
// followed by a random string of characters.
|
||||
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
|
||||
@interface SQRLUpdater ()
|
||||
|
||||
@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 directory manager used for downloads and unpacks.
|
||||
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
|
||||
|
||||
// The code signature for the running application, used to check updates before
|
||||
// sending them to ShipIt.
|
||||
@@ -76,22 +69,25 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
// errors, on a background thread.
|
||||
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update;
|
||||
|
||||
// Downloads the archived bundle associated with the given update.
|
||||
// Downloads the update archive for the given update.
|
||||
//
|
||||
// update - Describes the update to install. This must not be nil.
|
||||
// downloadDirectory - A directory in which to create a temporary directory for this
|
||||
// download. This must not be nil.
|
||||
// downloadDirectory - A directory in which to create a temporary directory for
|
||||
// this download. This must not be nil.
|
||||
//
|
||||
// Returns a signal which sends an unarchived `NSBundle` then completes, or
|
||||
// errors, on a background thread.
|
||||
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
|
||||
// Returns a signal which sends an NSURL of the downloaded update archive then
|
||||
// completes, or errors, on a background thread.
|
||||
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
|
||||
|
||||
// Creates a unique directory in which to save the update bundle, for later use
|
||||
// by ShipIt.
|
||||
// Unpacks an update archive.
|
||||
//
|
||||
// Returns a signal which sends an `NSURL` then completes, or errors, on an
|
||||
// unspecified thread.
|
||||
- (RACSignal *)uniqueTemporaryDirectoryForUpdate;
|
||||
// archiveURL - The location of the update archive, currently only ZIP
|
||||
// archives are supported. Must not be nil.
|
||||
// directoryURL - A directory in which to place the unpacked contents of the
|
||||
// archive. Must not be nil.
|
||||
//
|
||||
// Returns a signal which completes or errors.
|
||||
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL;
|
||||
|
||||
// Recursively searches the given directory for an application bundle that has
|
||||
// the same identifier as the running application.
|
||||
@@ -124,16 +120,27 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
// Returns a signal which completes or errors on a background thread.
|
||||
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update;
|
||||
|
||||
// Verifies that an existing state is innocuous, and therefore safe to
|
||||
// overwrite.
|
||||
//
|
||||
// This won't be the case if, for example, an update is currently being
|
||||
// installed.
|
||||
//
|
||||
// Returns a signal which sends `existingState` then completes upon successful
|
||||
// validation, or errors otherwise.
|
||||
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLUpdater
|
||||
@implementation SQRLUpdater {
|
||||
RACSubject *_updates;
|
||||
}
|
||||
|
||||
#pragma mark Properties
|
||||
|
||||
- (RACSignal *)updates {
|
||||
return [[self.checkForUpdatesCommand.executionSignals
|
||||
concat]
|
||||
setNameWithFormat:@"%@ -updates", self];
|
||||
return [_updates
|
||||
deliverOn:RACScheduler.mainThreadScheduler];
|
||||
}
|
||||
|
||||
#pragma mark Lifecycle
|
||||
@@ -144,127 +151,57 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
}
|
||||
|
||||
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest {
|
||||
return [self initWithUpdateRequest:updateRequest requestForDownload:^(NSURL *downloadURL) {
|
||||
return [NSURLRequest requestWithURL:downloadURL];
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload {
|
||||
NSParameterAssert(updateRequest != nil);
|
||||
NSParameterAssert(requestForDownload != nil);
|
||||
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_requestForDownload = [requestForDownload copy];
|
||||
_updateRequest = [updateRequest copy];
|
||||
_updateClass = SQRLUpdate.class;
|
||||
|
||||
_directoryManager = SQRLDirectoryManager.currentApplicationManager;
|
||||
|
||||
NSError *error = nil;
|
||||
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
if (_signature == nil) {
|
||||
#if DEBUG
|
||||
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
|
||||
return nil;
|
||||
#else
|
||||
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
|
||||
#endif
|
||||
}
|
||||
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
|
||||
|
||||
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
|
||||
@weakify(self);
|
||||
|
||||
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(self.updateRequest != nil);
|
||||
|
||||
// TODO: Maybe allow this to be an argument to the command?
|
||||
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
return [[[[[[[[self
|
||||
performHousekeeping]
|
||||
then:^{
|
||||
self.state = SQRLUpdaterStateCheckingForUpdate;
|
||||
|
||||
return [NSURLConnection rac_sendAsynchronousRequest:request];
|
||||
}]
|
||||
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
|
||||
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
|
||||
NSHTTPURLResponse *httpResponse = (id)response;
|
||||
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
|
||||
SQRLUpdaterServerDataErrorKey: bodyData,
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
if (httpResponse.statusCode == 204 /* No Content */) {
|
||||
return [RACSignal empty];
|
||||
}
|
||||
|
||||
BOOL readOnlyVolume = [self isRunningOnReadOnlyVolume];
|
||||
if (readOnlyVolume) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update while running on a read-only volume", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The application is on a read-only volume. Please move the application and try again. If you're on macOS Sierra or later, you'll need to move the application out of the Downloads directory. See https://github.com/Squirrel/Squirrel.Mac/issues/182 for more information.", nil),
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorReadOnlyVolume userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}
|
||||
|
||||
return [RACSignal return:bodyData];
|
||||
}]
|
||||
flatten]
|
||||
flattenMap:^(NSData *data) {
|
||||
return [self updateFromJSONData:data];
|
||||
}]
|
||||
flattenMap:^(SQRLUpdate *update) {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
self.state = SQRLUpdaterStateDownloadingUpdate;
|
||||
|
||||
return [self downloadAndPrepareUpdate:update];
|
||||
}]
|
||||
doCompleted:^{
|
||||
self.state = SQRLUpdaterStateAwaitingRelaunch;
|
||||
}];
|
||||
}]
|
||||
finally:^{
|
||||
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return;
|
||||
self.state = SQRLUpdaterStateIdle;
|
||||
}]
|
||||
deliverOn:RACScheduler.mainThreadScheduler];
|
||||
}];
|
||||
|
||||
_shipItLauncher = [[[RACSignal
|
||||
_checkForUpdatesAction = [[[RACSignal
|
||||
defer:^{
|
||||
@strongify(self);
|
||||
return [RACSignal return:self.updateRequest];
|
||||
}]
|
||||
flattenMap:^(NSURLRequest *request) {
|
||||
@strongify(self);
|
||||
return [self checkForUpdates:request];
|
||||
}]
|
||||
action];
|
||||
|
||||
_updates = [[RACSubject
|
||||
subject]
|
||||
setNameWithFormat:@"%@ updates", self];
|
||||
|
||||
_shipItLauncher = [[[[RACSignal
|
||||
defer:^{
|
||||
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
|
||||
|
||||
BOOL targetWritable = [self canWriteToURL:targetURL];
|
||||
BOOL parentWritable = [self canWriteToURL:targetURL.URLByDeletingLastPathComponent];
|
||||
return [SQRLShipItLauncher launchPrivileged:!targetWritable || !parentWritable];
|
||||
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]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
deferred]
|
||||
setNameWithFormat:@"shipItLauncher"];
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)canWriteToURL:(NSURL *)fileURL {
|
||||
NSNumber *writable = nil;
|
||||
NSError *writableError = nil;
|
||||
BOOL gotWritable = [fileURL getResourceValue:&writable forKey:NSURLIsWritableKey error:&writableError];
|
||||
// If we can't determine whether it can be written, assume nonprivileged and
|
||||
// wait for another, more canonical error.
|
||||
return !gotWritable || writable.boolValue;
|
||||
- (void)dealloc {
|
||||
[_updates sendCompleted];
|
||||
}
|
||||
|
||||
#pragma mark Checking for Updates
|
||||
@@ -272,20 +209,64 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval {
|
||||
@weakify(self);
|
||||
|
||||
return [[[[[RACSignal
|
||||
return [[[[RACSignal
|
||||
interval:interval onScheduler:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]]
|
||||
flattenMap:^(id _) {
|
||||
@strongify(self);
|
||||
return [[self.checkForUpdatesCommand
|
||||
execute:RACUnit.defaultUnit]
|
||||
|
||||
return [[[self.checkForUpdatesAction
|
||||
deferred]
|
||||
ignoreValues]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error checking for updates: %@", error);
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
}]
|
||||
takeUntil:self.rac_willDeallocSignal]
|
||||
publish]
|
||||
connect];
|
||||
subscribeCompleted:^{}];
|
||||
}
|
||||
|
||||
- (RACSignal *)checkForUpdates:(NSURLRequest *)request {
|
||||
NSParameterAssert(request != nil);
|
||||
|
||||
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
|
||||
if (updatesDisabled) return [RACSignal empty];
|
||||
|
||||
NSMutableURLRequest *newRequest = [request mutableCopy];
|
||||
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
return [[[[[[NSURLConnection
|
||||
rac_sendAsynchronousRequest:newRequest]
|
||||
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
|
||||
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
|
||||
NSHTTPURLResponse *httpResponse = (id)response;
|
||||
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
|
||||
SQRLUpdaterServerDataErrorKey: bodyData,
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
if (httpResponse.statusCode == 204 /* No Content */) {
|
||||
return [RACSignal empty];
|
||||
}
|
||||
}
|
||||
|
||||
return [RACSignal return:bodyData];
|
||||
}]
|
||||
flatten]
|
||||
flattenMap:^(NSData *data) {
|
||||
return [self updateFromJSONData:data];
|
||||
}]
|
||||
flattenMap:^(SQRLUpdate *update) {
|
||||
return [self downloadAndPrepareUpdate:update];
|
||||
}]
|
||||
doNext:^(SQRLDownloadedUpdate *update) {
|
||||
[self->_updates sendNext:update];
|
||||
}];
|
||||
}
|
||||
|
||||
- (RACSignal *)updateFromJSONData:(NSData *)data {
|
||||
@@ -327,144 +308,93 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
setNameWithFormat:@"%@ -updateFromJSONData:", self];
|
||||
}
|
||||
|
||||
#pragma Download
|
||||
|
||||
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update {
|
||||
NSParameterAssert(update != nil);
|
||||
|
||||
return [[[self
|
||||
uniqueTemporaryDirectoryForUpdate]
|
||||
return [[[self.directoryManager
|
||||
createUniqueUpdateDirectoryURL]
|
||||
flattenMap:^(NSURL *downloadDirectory) {
|
||||
void (^cleanUp)(void) = ^{
|
||||
NSError *error;
|
||||
if (![NSFileManager.defaultManager removeItemAtURL:downloadDirectory error:&error]) {
|
||||
NSLog(@"Error removing temporary download directory at %@: %@", downloadDirectory, error.sqrl_verboseDescription);
|
||||
}
|
||||
};
|
||||
|
||||
return [[[self
|
||||
downloadBundleForUpdate:update intoDirectory:downloadDirectory]
|
||||
return [[[[[[self
|
||||
downloadArchiveForUpdate:update intoDirectory:downloadDirectory]
|
||||
flattenMap:^(NSURL *updateArchiveURL) {
|
||||
return [self unpackArchiveAtURL:updateArchiveURL intoDirectory:downloadDirectory];
|
||||
}]
|
||||
ignoreValues]
|
||||
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
|
||||
flattenMap:^(NSBundle *updateBundle) {
|
||||
// If the bundle is nil it means our conditional GET told us
|
||||
// we already downloaded the update. So just clean up.
|
||||
if (updateBundle == nil) {
|
||||
cleanUp();
|
||||
return [RACSignal empty];
|
||||
}
|
||||
|
||||
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
|
||||
}]
|
||||
doError:^(id _) {
|
||||
cleanUp();
|
||||
NSError *error = nil;
|
||||
if (![NSFileManager.defaultManager removeItemAtURL:downloadDirectory error:&error]) {
|
||||
NSLog(@"Error removing temporary download directory at %@: %@", downloadDirectory, error.sqrl_verboseDescription);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
|
||||
}
|
||||
|
||||
- (RACSignal *)unarchiveAndPrepareData:(NSData *)data withName:(NSString *)name intoDirectory:(NSURL *)downloadDirectory {
|
||||
return [[[[[RACSignal
|
||||
defer:^{
|
||||
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:name];
|
||||
NSError *error = nil;
|
||||
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
|
||||
return [RACSignal return:zipOutputURL];
|
||||
} else {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}]
|
||||
doNext:^(NSURL *zipOutputURL) {
|
||||
NSLog(@"Download completed to: %@", zipOutputURL);
|
||||
}]
|
||||
flattenMap:^(NSURL *zipOutputURL) {
|
||||
return [[[[SQRLZipArchiver
|
||||
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:zipOutputURL]]
|
||||
doCompleted:^{
|
||||
NSError *error = nil;
|
||||
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
|
||||
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
flattenMap:^(NSURL *zipOutputURL) {
|
||||
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -unarchiveAndPrepareData:withName: %@ intoDirectory: %@", self, name, downloadDirectory];
|
||||
}
|
||||
|
||||
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
|
||||
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
|
||||
NSParameterAssert(update != nil);
|
||||
NSParameterAssert(downloadDirectory != nil);
|
||||
|
||||
return [[RACSignal
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
NSURL *zipDownloadURL = update.updateURL;
|
||||
NSMutableURLRequest *zipDownloadRequest = [self.requestForDownload(zipDownloadURL) mutableCopy];
|
||||
|
||||
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) {
|
||||
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:zipDownloadRequest];
|
||||
|
||||
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:self.directoryManager];
|
||||
|
||||
return [[[[connection
|
||||
download:downloadManager]
|
||||
reduceEach:^(NSURLResponse *response, NSURL *bodyLocation) {
|
||||
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
|
||||
NSHTTPURLResponse *httpResponse = (id)response;
|
||||
|
||||
if (httpResponse.statusCode == 304 /* Not Modified */) {
|
||||
return [RACSignal return:nil];
|
||||
}
|
||||
|
||||
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
|
||||
SQRLUpdaterServerDataErrorKey: bodyData,
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
self.etag = httpResponse.allHeaderFields[@"ETag"];
|
||||
}
|
||||
|
||||
return [self unarchiveAndPrepareData:bodyData withName:zipDownloadURL.lastPathComponent intoDirectory:downloadDirectory];
|
||||
return [[RACSignal
|
||||
return:bodyLocation]
|
||||
tryMap:^ NSURL * (NSURL *bodyLocation, NSError **errorRef) {
|
||||
NSURL *updateLocation = [downloadDirectory URLByAppendingPathComponent:response.suggestedFilename];
|
||||
if (![NSFileManager.defaultManager moveItemAtURL:bodyLocation toURL:updateLocation error:errorRef]) return nil;
|
||||
return updateLocation;
|
||||
}];
|
||||
}]
|
||||
flatten];
|
||||
flatten]
|
||||
concat:[[downloadManager
|
||||
removeAllResumableDownloads]
|
||||
catchTo:RACSignal.empty]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
|
||||
doNext:^(NSURL *zipOutputURL) {
|
||||
NSLog(@"Download completed to: %@", zipOutputURL);
|
||||
}]
|
||||
setNameWithFormat:@"%@ -downloadArchiveForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
|
||||
}
|
||||
|
||||
#pragma mark File Management
|
||||
|
||||
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
|
||||
return [[[RACSignal
|
||||
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL {
|
||||
NSParameterAssert(archiveURL != nil);
|
||||
NSParameterAssert(directoryURL != nil);
|
||||
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [directoryManager storageURL];
|
||||
return [SQRLZipArchiver unzipArchiveAtURL:archiveURL intoDirectoryAtURL:directoryURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *storageURL) {
|
||||
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
|
||||
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
|
||||
@onExit {
|
||||
free(updateDirectoryCString);
|
||||
};
|
||||
|
||||
if (mkdtemp(updateDirectoryCString) == NULL) {
|
||||
int code = errno;
|
||||
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not create temporary directory", nil),
|
||||
NSURLErrorKey: updateDirectoryTemplate
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
|
||||
}
|
||||
|
||||
NSString *updateDirectoryPath = [NSFileManager.defaultManager stringWithFileSystemRepresentation:updateDirectoryCString length:strlen(updateDirectoryCString)];
|
||||
return [RACSignal return:[NSURL fileURLWithPath:updateDirectoryPath isDirectory:YES]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -uniqueTemporaryDirectoryForUpdate", self];
|
||||
setNameWithFormat:@"%@ unpackArchiveAtURL: %@ intoDirectory: %@", self, archiveURL, directoryURL];
|
||||
}
|
||||
|
||||
- (RACSignal *)updateBundleMatchingCurrentApplicationInDirectory:(NSURL *)directory {
|
||||
@@ -477,25 +407,28 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, directory, error);
|
||||
return YES;
|
||||
}];
|
||||
|
||||
NSURL *updateBundleURL = [enumerator.rac_sequence objectPassingTest:^(NSURL *URL) {
|
||||
NSString *type = nil;
|
||||
NSError *error = nil;
|
||||
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
|
||||
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
|
||||
NSURL *updateBundleURL = [[[enumerator.rac_promise
|
||||
start]
|
||||
filter:^(NSURL *URL) {
|
||||
NSString *type = nil;
|
||||
NSError *error = nil;
|
||||
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
|
||||
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSBundle *bundle = [NSBundle bundleWithURL:URL];
|
||||
if (bundle == nil) {
|
||||
NSLog(@"Could not open application bundle at %@", URL);
|
||||
return NO;
|
||||
}
|
||||
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
|
||||
|
||||
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
|
||||
}];
|
||||
NSBundle *bundle = [NSBundle bundleWithURL:URL];
|
||||
if (bundle == nil) {
|
||||
NSLog(@"Could not open application bundle at %@", URL);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
|
||||
}]
|
||||
first];
|
||||
|
||||
if (updateBundleURL != nil) {
|
||||
return [RACSignal return:updateBundleURL];
|
||||
@@ -522,100 +455,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
setNameWithFormat:@"%@ -shipItStateURL", self];
|
||||
}
|
||||
|
||||
/// Is the host app running on a read-only volume?
|
||||
- (BOOL)isRunningOnReadOnlyVolume {
|
||||
struct statfs statfsInfo;
|
||||
NSURL *bundleURL = NSRunningApplication.currentApplication.bundleURL;
|
||||
int result = statfs(bundleURL.fileSystemRepresentation, &statfsInfo);
|
||||
if (result == 0) {
|
||||
return (statfsInfo.f_flags & MNT_RDONLY) != 0;
|
||||
} else {
|
||||
// If we can't even check if the volume is read-only, assume it is.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
- (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:^{
|
||||
// If we already have updates downloaded we don't wanna prune them.
|
||||
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
|
||||
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [directoryManager storageURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *storageURL) {
|
||||
NSFileManager *manager = [[NSFileManager alloc] init];
|
||||
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
|
||||
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
|
||||
return YES;
|
||||
}];
|
||||
|
||||
return [[enumerator.rac_sequence.signal
|
||||
filter:^(NSURL *enumeratedURL) {
|
||||
NSString *name = enumeratedURL.lastPathComponent;
|
||||
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
|
||||
}]
|
||||
doNext:^(NSURL *directoryURL) {
|
||||
NSError *error = nil;
|
||||
if (![manager removeItemAtURL:directoryURL error:&error]) {
|
||||
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
|
||||
}
|
||||
|
||||
|
||||
// Like truncation, but backwards.
|
||||
- (RACSignal *)backwardTruncateFile:(NSURL *)fileURL {
|
||||
return [RACSignal defer:^{
|
||||
static const NSInteger MAX_LENGTH = 1024 * 1024 * 8;
|
||||
|
||||
NSError *error;
|
||||
NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:fileURL error:&error];
|
||||
if (handle == nil) return [RACSignal error:error];
|
||||
|
||||
unsigned long long fileLength = [handle seekToEndOfFile];
|
||||
if (fileLength <= MAX_LENGTH) return [RACSignal empty];
|
||||
|
||||
[handle seekToFileOffset:fileLength - MAX_LENGTH];
|
||||
NSData *mostRecentData = [handle readDataToEndOfFile];
|
||||
[handle truncateFileAtOffset:0];
|
||||
[handle writeData:mostRecentData];
|
||||
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
}
|
||||
|
||||
- (RACSignal *)truncateLogs {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [RACSignal zip:@[ [directoryManager shipItStdoutURL], [directoryManager shipItStderrURL] ]];
|
||||
}]
|
||||
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL) {
|
||||
return [RACSignal merge:@[ [self backwardTruncateFile:stdoutURL], [self backwardTruncateFile:stderrURL] ]];
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark Installing Updates
|
||||
|
||||
- (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle *)updateBundle {
|
||||
@@ -624,47 +463,62 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
|
||||
return [[[[self.signature
|
||||
verifyBundleAtURL:updateBundle.bundleURL]
|
||||
then:^{
|
||||
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle];
|
||||
return [RACSignal return:downloadedUpdate];
|
||||
}]
|
||||
concat:[RACSignal return:[[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle]]]
|
||||
flattenMap:^(SQRLDownloadedUpdate *downloadedUpdate) {
|
||||
return [[self prepareUpdateForInstallation:downloadedUpdate] then:^{
|
||||
return [RACSignal return:downloadedUpdate];
|
||||
}];
|
||||
return [[self
|
||||
prepareUpdateForInstallation:downloadedUpdate]
|
||||
concat:[RACSignal return:downloadedUpdate]];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -verifyAndPrepareUpdate: %@ fromBundle: %@", self, update, updateBundle];
|
||||
}
|
||||
|
||||
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
|
||||
// If this happens, shit is crazy, because it implies that an
|
||||
// update is being installed over us right now.
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:existingState];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -validateExistingState: %@", self, existingState];
|
||||
}
|
||||
|
||||
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update {
|
||||
NSParameterAssert(update != nil);
|
||||
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
|
||||
NSBundle *appBundle = [NSBundle bundleWithURL:currentApplication.bundleURL];
|
||||
// Only use the update bundle's name if the user hasn't renamed the
|
||||
// app themselves.
|
||||
BOOL useUpdateBundleName = [appBundle.sqrl_executableName isEqual:currentApplication.bundleURL.lastPathComponent.stringByDeletingPathExtension];
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO useUpdateBundleName:useUpdateBundleName];
|
||||
return [request writeUsingURL:self.shipItStateURL];
|
||||
}]
|
||||
then:^{
|
||||
return self.shipItLauncher;
|
||||
return [[[[[[[[SQRLShipItState
|
||||
readUsingURL:self.shipItStateURL]
|
||||
catchTo:[RACSignal empty]]
|
||||
flattenMap:^(SQRLShipItState *existingState) {
|
||||
return [self validateExistingState:existingState];
|
||||
}]
|
||||
ignoreValues]
|
||||
concat:[RACSignal defer:^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
|
||||
return [state writeUsingURL:self.shipItStateURL];
|
||||
}]]
|
||||
concat:self.shipItLauncher]
|
||||
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
|
||||
return [[[[[[[[[SQRLShipItState
|
||||
readUsingURL:self.shipItStateURL]
|
||||
map:^(SQRLShipItRequest *request) {
|
||||
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES useUpdateBundleName:request.useUpdateBundleName];
|
||||
flattenMap:^(SQRLShipItState *existingState) {
|
||||
return [self validateExistingState:existingState];
|
||||
}]
|
||||
flattenMap:^(SQRLShipItRequest *request) {
|
||||
return [[request
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
state.relaunchAfterInstallation = YES;
|
||||
return [[state
|
||||
writeUsingURL:self.shipItStateURL]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
|
||||
}]
|
||||
@@ -675,7 +529,8 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
// Never allow `completed` to escape this signal chain (in case
|
||||
// -terminate: is asynchronous or something crazy).
|
||||
concat:[RACSignal never]]
|
||||
replay]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
deferred]
|
||||
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
|
||||
}
|
||||
|
||||
|
||||
+12
-14
@@ -130,25 +130,23 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
|
||||
// This is important because the signals on `self` complete upon
|
||||
// dealloc.
|
||||
return:self]
|
||||
then:^{
|
||||
return [RACSignal
|
||||
zip:@[ self.taskTerminated, self.standardErrorData ]
|
||||
reduce:^(NSNumber *exitStatus, NSData *errorData) {
|
||||
if (exitStatus.intValue == 0) return [RACSignal empty];
|
||||
ignoreValues]
|
||||
concat:[RACSignal
|
||||
zip:@[ self.taskTerminated, self.standardErrorData ]
|
||||
reduce:^(NSNumber *exitStatus, NSData *errorData) {
|
||||
if (exitStatus.intValue == 0) return [RACSignal empty];
|
||||
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
|
||||
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
|
||||
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
|
||||
|
||||
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
|
||||
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
|
||||
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
|
||||
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
|
||||
}];
|
||||
}]
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
|
||||
}]]
|
||||
take:1]
|
||||
flatten]
|
||||
replay]
|
||||
setNameWithFormat:@"-launchWithArguments: %@", arguments];
|
||||
|
||||
self.dittoTask.arguments = arguments;
|
||||
|
||||
+102
-114
@@ -7,18 +7,15 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import "EXTScope.h"
|
||||
#import <ReactiveCocoa/RACCommand.h>
|
||||
#import <ReactiveCocoa/RACSignal+Operations.h>
|
||||
#import <ReactiveCocoa/RACScheduler.h>
|
||||
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import "RACSignal+SQRLTransactionExtensions.h"
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLTerminationListener.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
// The maximum number of times ShipIt should run the same installation state, in
|
||||
// an attempt to update.
|
||||
@@ -30,133 +27,124 @@ static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
|
||||
// The domain for errors generated here.
|
||||
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
|
||||
|
||||
static NSUInteger installationAttempts(NSString *applicationIdentifier) {
|
||||
return CFPreferencesGetAppIntegerValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFStringRef)applicationIdentifier, NULL);
|
||||
}
|
||||
|
||||
static BOOL setInstallationAttempts(NSString *applicationIdentifier, NSUInteger attempts) {
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(attempts), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
}
|
||||
|
||||
static BOOL clearInstallationAttempts(NSString *applicationIdentifier) {
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, NULL, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
}
|
||||
|
||||
// Waits for all instances of the target application (as described in the
|
||||
// `request`) to exit, then sends completed.
|
||||
static RACSignal *waitForTerminationIfNecessary(SQRLShipItRequest *request) {
|
||||
// `state`) to exit, then sends completed.
|
||||
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
if (request.bundleIdentifier == nil) return [RACSignal empty];
|
||||
if (state.bundleIdentifier == nil) return [RACSignal empty];
|
||||
|
||||
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier];
|
||||
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
|
||||
return [listener waitForTermination];
|
||||
}]
|
||||
setNameWithFormat:@"waitForTerminationIfNecessary"];
|
||||
}
|
||||
|
||||
static void installRequest(RACSignal *readRequestSignal, NSString *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 < 3) {
|
||||
NSLog(@"Missing launchd job label or state path for ShipIt");
|
||||
if (argc < 2) {
|
||||
NSLog(@"Missing launchd job label for ShipIt");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
char const *jobLabel = argv[1];
|
||||
const char *statePath = argv[2];
|
||||
NSURL *shipItStateURL = [NSURL fileURLWithPath:@(statePath)];
|
||||
installRequest([SQRLShipItRequest readUsingURL:[RACSignal return:shipItStateURL]], @(jobLabel));
|
||||
const char *jobLabel = argv[1];
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
|
||||
RACSignal *stateLocation = directoryManager.shipItStateURL;
|
||||
|
||||
[[[[[[[SQRLShipItState
|
||||
readUsingURL:stateLocation]
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
return waitForTerminationIfNecessary(state);
|
||||
}]
|
||||
ignoreValues]
|
||||
// Read the latest state, in case it was modified by the
|
||||
// controlling application in the meantime.
|
||||
concat:[SQRLShipItState readUsingURL:stateLocation]]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error reading saved installer state: %@", error);
|
||||
|
||||
// Exit successfully so launchd doesn't restart us again.
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
BOOL freshInstall = (state.installerState == SQRLInstallerStateNothingToDo);
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:directoryManager];
|
||||
|
||||
NSUInteger attempt = (freshInstall ? 1 : state.installationStateAttempt + 1);
|
||||
RACSignal *action;
|
||||
|
||||
if (attempt > SQRLShipItMaximumInstallationAttempts) {
|
||||
action = [[RACSignal
|
||||
defer:^{
|
||||
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
|
||||
|
||||
return [installer abortInstallationWithState:state];
|
||||
}]
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error aborting installation: %@", error);
|
||||
|
||||
// Exit successfully so launchd doesn't restart us again.
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
} else {
|
||||
action = [[[[RACSignal
|
||||
defer:^{
|
||||
if (freshInstall) {
|
||||
NSLog(@"Beginning installation");
|
||||
state.installerState = SQRLInstallerStateClearingQuarantine;
|
||||
} else {
|
||||
NSLog(@"Resuming installation from state %i", (int)state.installerState);
|
||||
}
|
||||
|
||||
state.installationStateAttempt = attempt;
|
||||
|
||||
return [state writeUsingURL:stateLocation];
|
||||
}]
|
||||
concat:[installer installUpdateWithState:state]]
|
||||
doCompleted:^{
|
||||
NSLog(@"Installation completed successfully");
|
||||
}]
|
||||
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
|
||||
}
|
||||
|
||||
if (state.relaunchAfterInstallation) {
|
||||
// Relaunch regardless of whether installation succeeds or
|
||||
// fails.
|
||||
action = [[action
|
||||
deliverOn:RACScheduler.mainThreadScheduler]
|
||||
doFinished:^{
|
||||
NSURL *bundleURL = state.targetBundleURL;
|
||||
if (bundleURL == nil) {
|
||||
NSLog(@"Missing target bundle URL, cannot relaunch application");
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
|
||||
NSLog(@"Could not relaunch application at %@: %@", bundleURL, error);
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"Application relaunched at %@", bundleURL);
|
||||
}];
|
||||
}
|
||||
|
||||
return action;
|
||||
}]
|
||||
subscribeError:^(NSError *error) {
|
||||
NSLog(@"Installation error: %@", error);
|
||||
exit(EXIT_FAILURE);
|
||||
} completed:^{
|
||||
exit(EXIT_SUCCESS);
|
||||
}];
|
||||
|
||||
dispatch_main();
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// Prefix header for all source files of the 'Squirrel' target in the 'Squirrel' project
|
||||
//
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
#endif
|
||||
@@ -6,14 +6,6 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Squirrel.
|
||||
FOUNDATION_EXPORT double SquirrelVersionNumber;
|
||||
|
||||
//! Project version string for Squirrel.
|
||||
FOUNDATION_EXPORT const unsigned char SquirrelVersionString[];
|
||||
|
||||
#import <Squirrel/NSBundle+SQRLVersionExtensions.h>
|
||||
#import <Squirrel/NSProcessInfo+SQRLVersionExtensions.h>
|
||||
#import <Squirrel/SQRLDownloadedUpdate.h>
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/* Localized versions of Info.plist keys */
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// NSHTTPURLResponseExtensionsSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 04/12/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSHTTPURLResponse+SQRLExtensions.h"
|
||||
|
||||
SpecBegin(NSHTTPURLResponseExtensions)
|
||||
|
||||
it(@"should perform case insensitive key look up", ^{
|
||||
NSDictionary *headers = @{
|
||||
@"ETag": @"foo",
|
||||
};
|
||||
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://localhost"] statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:headers];
|
||||
|
||||
expect([response sqrl_valueForHTTPHeaderField:@"etag"]).to.equal(@"foo");
|
||||
expect([response sqrl_valueForHTTPHeaderField:@"ETAG"]).to.equal(@"foo");
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -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
|
||||
|
||||
+58
-65
@@ -6,16 +6,9 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
|
||||
QuickSpecBegin(SQRLCodeSignatureSpec)
|
||||
SpecBegin(SQRLCodeSignature)
|
||||
|
||||
__block NSBundle *bundle;
|
||||
__block void (^corruptURL)(NSURL *URL);
|
||||
@@ -24,26 +17,26 @@ beforeEach(^{
|
||||
bundle = self.testApplicationBundle;
|
||||
|
||||
corruptURL = ^(NSURL *URL) {
|
||||
expect(@([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL])).to(beTruthy());
|
||||
expect([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL]).to.beTruthy();
|
||||
};
|
||||
});
|
||||
|
||||
it(@"should verify a valid bundle", ^{
|
||||
NSError *error = nil;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
|
||||
it(@"should fail to verify with different code signing requirements", ^{
|
||||
NSError *error = nil;
|
||||
SQRLCodeSignature *signature = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
expect(signature).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
SQRLCodeSignature *verifier = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
expect(verifier).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
BOOL success = [[verifier verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(error).notTo.beNil();
|
||||
});
|
||||
|
||||
describe(@"code signature changes", ^{
|
||||
@@ -54,15 +47,15 @@ describe(@"code signature changes", ^{
|
||||
});
|
||||
|
||||
it(@"should fail to verify a bundle with a missing code signature", ^{
|
||||
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
|
||||
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
|
||||
it(@"should fail to verify a bundle with a corrupt code signature", ^{
|
||||
@@ -70,25 +63,25 @@ describe(@"code signature changes", ^{
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
});
|
||||
|
||||
describe(@"main executable changes", ^{
|
||||
it(@"should fail to verify a bundle with a missing executable", ^{
|
||||
expect(@([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL])).to(beTruthy());
|
||||
expect([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL]).to.beTruthy();
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo(beNil());
|
||||
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
|
||||
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// SQRLDownloadControllerSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 27/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDownloadManager.h"
|
||||
#import "SQRLResumableDownload.h"
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
SpecBegin(SQRLDownloadManager)
|
||||
|
||||
__block SQRLDownloadManager *downloadManager = nil;
|
||||
|
||||
beforeAll(^{
|
||||
downloadManager = SQRLDownloadManager.defaultDownloadManager;
|
||||
|
||||
NSError *removeError = nil;
|
||||
BOOL remove = [[downloadManager removeAllResumableDownloads] waitUntilCompleted:&removeError];
|
||||
if (!remove) {
|
||||
if ([removeError.domain isEqualToString:NSCocoaErrorDomain] && removeError.code == NSFileNoSuchFileError) return;
|
||||
|
||||
NSLog(@"Couldn’t remove resumable downloads %@", removeError.sqrl_verboseDescription);
|
||||
}
|
||||
});
|
||||
|
||||
NSURL * (^newTestURL)() = ^ () {
|
||||
return [[NSURL alloc] initWithScheme:@"http" host:@"localhost" path:[@"/" stringByAppendingString:NSProcessInfo.processInfo.globallyUniqueString]];
|
||||
};
|
||||
|
||||
NSURL * (^newDownloadURL)() = ^ () {
|
||||
NSError *error = nil;
|
||||
SQRLResumableDownload *download = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:newTestURL()]] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(download).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURL *downloadURL = download.fileURL;
|
||||
expect(downloadURL).notTo.beNil();
|
||||
|
||||
return downloadURL;
|
||||
};
|
||||
|
||||
it(@"should return a file that doesn't exist yet for new URLs", ^{
|
||||
NSURL *downloadURL = newDownloadURL();
|
||||
|
||||
BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:downloadURL.path];
|
||||
expect(exists).to.beFalsy();
|
||||
});
|
||||
|
||||
it(@"should return a path in a writable directory for new URLs", ^{
|
||||
NSURL *downloadURL = newDownloadURL();
|
||||
|
||||
BOOL writable = [NSFileManager.defaultManager isWritableFileAtPath:downloadURL.URLByDeletingLastPathComponent.path];
|
||||
expect(writable).to.beTruthy();
|
||||
});
|
||||
|
||||
it(@"should return the same path for the same URL", ^{
|
||||
NSURL *testURL = newTestURL();
|
||||
|
||||
SQRLDownload *download1 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
|
||||
SQRLDownload *download2 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
|
||||
expect(download1).notTo.beNil();
|
||||
expect(download1).to.equal(download2);
|
||||
});
|
||||
|
||||
it(@"should remember a response", ^{
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:newTestURL()];
|
||||
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:@{ @"ETag": NSProcessInfo.processInfo.globallyUniqueString }];
|
||||
|
||||
SQRLDownload *initialDownload = [[downloadManager downloadForRequest:request] first];
|
||||
|
||||
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:request response:response fileURL:initialDownload.fileURL];
|
||||
expect(newDownload).notTo.equal(initialDownload);
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL setNewDownload = [[downloadManager setDownload:newDownload forRequest:request] waitUntilCompleted:&error];
|
||||
expect(setNewDownload).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
SQRLResumableDownload *resumedDownload = [[downloadManager downloadForRequest:request] first];
|
||||
expect(newDownload).to.equal(resumedDownload);
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// SQRLDownloadedUpdateSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 04/12/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
SpecBegin(SQRLDownloadedUpdate)
|
||||
|
||||
it(@"should serialise to JSON", ^{
|
||||
NSError *error = nil;
|
||||
SQRLUpdate *update = [SQRLUpdate modelWithDictionary:@{
|
||||
@keypath(SQRLUpdate.new, updateURL) : [NSURL URLWithString:@"http://fake.domain/path"],
|
||||
} error:&error];
|
||||
expect(update).notTo.beNil();
|
||||
|
||||
NSURL *testApplicationURL = [self createTestApplicationUpdate];
|
||||
NSBundle *testApplicationBundle = [NSBundle bundleWithURL:testApplicationURL];
|
||||
expect(testApplicationBundle).notTo.beNil();
|
||||
|
||||
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:testApplicationBundle];
|
||||
expect(downloadedUpdate).notTo.beNil();
|
||||
|
||||
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:downloadedUpdate];
|
||||
expect(JSONDictionary).notTo.beNil();
|
||||
|
||||
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
|
||||
expect(JSONData).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
JSONDictionary = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:&error];
|
||||
expect(JSONDictionary).notTo.beNil();
|
||||
|
||||
SQRLDownloadedUpdate *downloadedUpdate2 = [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:&error];
|
||||
expect(downloadedUpdate2).to.equal(downloadedUpdate);
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// SQRLHTTPServer.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 03/12/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface SQRLHTTPServer : NSObject
|
||||
|
||||
- (NSURL *)start:(NSError **)errorRef;
|
||||
- (void)invalidate;
|
||||
|
||||
@property (copy, nonatomic) CFHTTPMessageRef (^responseBlock)(CFHTTPMessageRef request);
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,165 @@
|
||||
//
|
||||
// SQRLHTTPServer.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 03/12/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLHTTPServer.h"
|
||||
|
||||
#import <sys/socket.h>
|
||||
#import <netinet/in.h>
|
||||
|
||||
@interface SQRLHTTPServer ()
|
||||
@property (assign, nonatomic) dispatch_source_t listenerSource;
|
||||
@property (strong, nonatomic) NSMutableArray *cleanupBlocks;
|
||||
@end
|
||||
|
||||
@implementation SQRLHTTPServer
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_cleanupBlocks = [[NSMutableArray alloc] init];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self invalidate];
|
||||
}
|
||||
|
||||
- (NSURL *)start:(NSError **)errorRef {
|
||||
NSParameterAssert(self.listenerSource == NULL);
|
||||
|
||||
int listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
|
||||
struct sockaddr_in listenAddress = {
|
||||
.sin_len = sizeof(listenAddress),
|
||||
.sin_family = AF_INET,
|
||||
.sin_port = 0,
|
||||
.sin_addr = {
|
||||
.s_addr = htonl(INADDR_LOOPBACK),
|
||||
},
|
||||
};
|
||||
int bindError = bind(listenSocket, (struct sockaddr const *)&listenAddress, listenAddress.sin_len);
|
||||
if (bindError != 0) {
|
||||
close(listenSocket);
|
||||
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:bindError userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
listen(listenSocket, 128);
|
||||
|
||||
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listenSocket, 0, NULL);
|
||||
dispatch_source_set_event_handler(source, ^{
|
||||
int connectionSocket = accept(listenSocket, NULL, NULL);
|
||||
// Wait to close the connection until after the test case is complete.
|
||||
// We need NSURLConnection to timeout, closing the connection causes it
|
||||
// to error immediately.
|
||||
[self addCleanupBlock:^{
|
||||
close(connectionSocket);
|
||||
}];
|
||||
|
||||
CFHTTPMessageRef request = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true));
|
||||
|
||||
while (1) {
|
||||
uint8_t buffer;
|
||||
ssize_t readLength = read(connectionSocket, &buffer, 1);
|
||||
if (readLength < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CFHTTPMessageAppendBytes(request, (UInt8 const *)&buffer, readLength);
|
||||
if (!CFHTTPMessageIsHeaderComplete(request)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Doesn't support reading requests with a body for simplicity
|
||||
NSString *contentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Content-Length")));
|
||||
if (contentLength != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
NSLog(@"Received Request:");
|
||||
NSLog(@"%@", [[NSString alloc] initWithData:CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)) encoding:NSASCIIStringEncoding]);
|
||||
|
||||
CFHTTPMessageRef response = self.responseBlock(request);
|
||||
NSData *responseData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(response));
|
||||
|
||||
NSLog(@"Sending Response:");
|
||||
NSLog(@"%@", [[NSString alloc] initWithData:responseData encoding:NSASCIIStringEncoding]);
|
||||
|
||||
size_t bufferLength = responseData.length;
|
||||
uint8_t const *buffer = responseData.bytes;
|
||||
|
||||
while (1) {
|
||||
ssize_t writeLength = write(connectionSocket, buffer, bufferLength);
|
||||
if (writeLength < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += writeLength;
|
||||
bufferLength -= writeLength;
|
||||
|
||||
if (bufferLength == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
dispatch_source_set_cancel_handler(source, ^{
|
||||
close(listenSocket);
|
||||
});
|
||||
dispatch_resume(source);
|
||||
|
||||
struct sockaddr_storage localAddress = {};
|
||||
socklen_t localAddressLength = sizeof(localAddress);
|
||||
int localAddressError = getsockname(listenSocket, (struct sockaddr *)&localAddress, &localAddressLength);
|
||||
if (localAddressError != 0) {
|
||||
dispatch_release(source);
|
||||
|
||||
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:localAddressError userInfo:nil];
|
||||
return nil;
|
||||
}
|
||||
|
||||
self.listenerSource = source;
|
||||
|
||||
// IPv4 and IPv6 address transport layer port fields are at the same offset
|
||||
// and are the same size
|
||||
in_port_t port = ntohs(((struct sockaddr_in *)&localAddress)->sin_port);
|
||||
|
||||
return [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%ld/", (unsigned long)port]];
|
||||
}
|
||||
|
||||
- (void)invalidate {
|
||||
dispatch_source_t listenerSource = self.listenerSource;
|
||||
if (listenerSource == NULL) return;
|
||||
|
||||
dispatch_source_cancel(listenerSource);
|
||||
dispatch_release(listenerSource);
|
||||
|
||||
self.listenerSource = NULL;
|
||||
|
||||
[self performCleanup];
|
||||
}
|
||||
|
||||
- (void)addCleanupBlock:(void (^)(void))block {
|
||||
[self.cleanupBlocks addObject:[block copy]];
|
||||
}
|
||||
|
||||
- (void)performCleanup {
|
||||
NSArray *blocks = [self.cleanupBlocks copy];
|
||||
[self.cleanupBlocks removeAllObjects];
|
||||
|
||||
for (void (^currentBlock)(void) in blocks) {
|
||||
currentBlock();
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -6,32 +6,13 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller+Private.h"
|
||||
#import "SQRLInstallerOwnedBundle.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItState.h"
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
|
||||
QuickSpecBegin(SQRLInstallerSpec)
|
||||
|
||||
mode_t (^modeOfURL)(NSURL *) = ^ mode_t (NSURL *fileURL) {
|
||||
NSFileSecurity *fileSecurity = nil;
|
||||
BOOL success = [fileURL getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:NULL];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(fileSecurity).notTo(beNil());
|
||||
|
||||
__block mode_t mode;
|
||||
expect(@(CFFileSecurityGetMode((__bridge CFFileSecurityRef)fileSecurity, &mode))).to(beTruthy());
|
||||
|
||||
return mode & (S_IRWXU | S_IRWXG | S_IRWXO);
|
||||
};
|
||||
SpecBegin(SQRLInstaller)
|
||||
|
||||
__block NSURL *updateURL;
|
||||
|
||||
@@ -39,148 +20,127 @@ beforeEach(^{
|
||||
updateURL = [self createTestApplicationUpdate];
|
||||
});
|
||||
|
||||
it(@"should install an update using ShipIt", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
it(@"should install an update", ^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
});
|
||||
|
||||
it(@"should install an update in process", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
|
||||
[self installWithRequest:request remote:NO];
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should install an update and relaunch", ^{
|
||||
NSString *bundleIdentifier = @"com.github.Squirrel.TestApplication";
|
||||
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier];
|
||||
expect(@(apps.count)).to(equal(@0));
|
||||
expect(apps.count).to.equal(0);
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.relaunchAfterInstallation = YES;
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count)).toEventually(equal(@1));
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count).will.equal(1);
|
||||
});
|
||||
|
||||
it(@"should install an update from another volume", ^{
|
||||
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication 2.1" fromDirectory:updateURL.URLByDeletingLastPathComponent];
|
||||
updateURL = [diskImageURL URLByAppendingPathComponent:updateURL.lastPathComponent];
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should install an update to another volume", ^{
|
||||
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication" fromDirectory:self.testApplicationURL.URLByDeletingLastPathComponent];
|
||||
NSURL *targetURL = [diskImageURL URLByAppendingPathComponent:self.testApplicationURL.lastPathComponent];
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
NSURL *plistURL = [targetURL URLByAppendingPathComponent:@"Contents/Info.plist"];
|
||||
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).withTimeout(SQRLLongTimeout).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"with backup restoration", ^{
|
||||
__block NSURL *targetURL;
|
||||
it(@"should install an update in process", ^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.installerState = SQRLInstallerStateClearingQuarantine;
|
||||
|
||||
beforeEach(^{
|
||||
targetURL = self.testApplicationURL;
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
|
||||
expect(installer).notTo.beNil();
|
||||
|
||||
NSURL *copiedTargetURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication Target.app"];
|
||||
expect(@([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:copiedTargetURL error:NULL])).to(beTruthy());
|
||||
|
||||
SQRLCodeSignature *codeSignature = self.testApplicationSignature;
|
||||
|
||||
SQRLInstallerOwnedBundle *ownedBundle = [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:targetURL temporaryURL:copiedTargetURL codeSignature:codeSignature];
|
||||
NSData *ownedBundleArchive = [NSKeyedArchiver archivedDataWithRootObject:ownedBundle];
|
||||
expect(ownedBundleArchive).notTo(beNil());
|
||||
|
||||
// Set up ShipIt's preferences like it paused in the middle of an
|
||||
// installation.
|
||||
NSString *applicationIdentifier = self.shipItDirectoryManager.applicationIdentifier;
|
||||
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(4), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFDataRef)ownedBundleArchive, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
|
||||
BOOL synchronized = CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
|
||||
expect(@(synchronized)).to(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should not install an update after too many attempts", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
__block NSError *error;
|
||||
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).toEventually(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
|
||||
});
|
||||
|
||||
it(@"should relaunch even after failing to install an update", ^{
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO];
|
||||
[self installWithRequest:request remote:YES];
|
||||
|
||||
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count)).toEventually(equal(@1));
|
||||
|
||||
__block NSError *error;
|
||||
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
|
||||
});
|
||||
NSError *installError = nil;
|
||||
BOOL install = [[installer installUpdateWithState:state] asynchronouslyWaitUntilCompleted:&installError];
|
||||
expect(install).to.beTruthy();
|
||||
expect(installError).to.beNil();
|
||||
});
|
||||
|
||||
it(@"should disallow writing the updated application except by the owner", ^{
|
||||
NSString *command = [NSString stringWithFormat:@"chmod -R 0777 '%@'", updateURL.path];
|
||||
expect(@(system(command.UTF8String))).to(equal(@0));
|
||||
it(@"should not install an update after too many attempts", ^{
|
||||
NSURL *targetURL = self.testApplicationURL;
|
||||
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
|
||||
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
|
||||
|
||||
expect(@(modeOfURL(updateURL))).to(equal(@0777));
|
||||
expect(@(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0777));
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.backupBundleURL = backupURL;
|
||||
state.installerState = SQRLInstallerStateInstalling;
|
||||
state.installationStateAttempt = 4;
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
[self launchShipIt];
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
// No update should've been installed, and the application should be
|
||||
// restored from the backup.
|
||||
__block NSError *error = nil;
|
||||
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
expect(@(modeOfURL(self.testApplicationURL))).to(equal(@0755));
|
||||
expect(@(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0755));
|
||||
it(@"should relaunch even after failing to install an update", ^{
|
||||
NSURL *targetURL = self.testApplicationURL;
|
||||
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
|
||||
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
|
||||
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
state.backupBundleURL = backupURL;
|
||||
state.installerState = SQRLInstallerStateInstalling;
|
||||
state.installationStateAttempt = 4;
|
||||
state.relaunchAfterInstallation = YES;
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self launchShipIt];
|
||||
|
||||
__block NSError *error = nil;
|
||||
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"signal handling", ^{
|
||||
__block NSURL *targetURL;
|
||||
|
||||
void (^verifyUpdate)(void) = ^{
|
||||
// Wait up to the launchd throttle interval, then verify that ShipIt
|
||||
// relaunched and finished installing the update.
|
||||
expect(self.testApplicationBundleVersion).withTimeout(SQRLLongTimeout).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
|
||||
NSError *error;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
};
|
||||
|
||||
beforeEach(^{
|
||||
// Copied so that we don't recreate the TestApplication bundle by
|
||||
// accessing the property.
|
||||
targetURL = self.testApplicationURL;
|
||||
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
|
||||
|
||||
[self installWithRequest:request remote:YES];
|
||||
[self launchShipIt];
|
||||
|
||||
// Wait until ShipIt has transitioned by at least one state.
|
||||
expect([[[SQRLShipItState readUsingURL:self.shipItDirectoryManager.shipItStateURL] asynchronousFirstOrDefault:nil success:NULL error:NULL] installerState]).willNot.equal(SQRLInstallerStateNothingToDo);
|
||||
|
||||
// Apply a random delay before sending the termination signal, to
|
||||
// fuzz out race conditions.
|
||||
@@ -188,42 +148,45 @@ describe(@"signal handling", ^{
|
||||
[NSThread sleepForTimeInterval:delay];
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
// Wait up to the launchd throttle interval, then verify that ShipIt
|
||||
// relaunched and finished installing the update.
|
||||
Expecta.asynchronousTestTimeout = 5;
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
|
||||
it(@"should handle SIGHUP", ^{
|
||||
system("killall -HUP ShipIt");
|
||||
verifyUpdate();
|
||||
});
|
||||
|
||||
it(@"should handle SIGTERM", ^{
|
||||
system("killall -TERM ShipIt");
|
||||
verifyUpdate();
|
||||
});
|
||||
|
||||
it(@"should handle SIGINT", ^{
|
||||
system("killall -INT ShipIt");
|
||||
verifyUpdate();
|
||||
});
|
||||
|
||||
it(@"should handle SIGQUIT", ^{
|
||||
system("killall -QUIT ShipIt");
|
||||
verifyUpdate();
|
||||
});
|
||||
|
||||
it(@"should handle SIGKILL", ^{
|
||||
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
|
||||
// a couple times to really test resumption.
|
||||
//
|
||||
// Two SIGKILL signals means that it'll be launched three times (which
|
||||
// matches the maximum number of attempts per state).
|
||||
for (int i = 0; i < 2; i++) {
|
||||
// a few times to really test resumption.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
system("killall -KILL ShipIt");
|
||||
|
||||
// Wait at least for the launchd throttle interval.
|
||||
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
|
||||
[NSThread sleepForTimeInterval:delay];
|
||||
}
|
||||
|
||||
verifyUpdate();
|
||||
});
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
//
|
||||
// SQRLShipItStateSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-09.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
|
||||
QuickSpecBegin(SQRLShipItRequestSpec)
|
||||
|
||||
__block SQRLDirectoryManager *directoryManager;
|
||||
__block SQRLShipItRequest *request;
|
||||
|
||||
beforeEach(^{
|
||||
directoryManager = SQRLDirectoryManager.currentApplicationManager;
|
||||
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
|
||||
expect(request).notTo(beNil());
|
||||
|
||||
expect(request.targetBundleURL).to(equal(self.testApplicationURL));
|
||||
expect(request.updateBundleURL).to(equal(updateURL));
|
||||
expect(request.bundleIdentifier).to(beNil());
|
||||
expect(@(request.launchAfterInstallation)).to(beFalsy());
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
NSURL *stateURL = [[directoryManager shipItStateURL] first];
|
||||
expect(stateURL).notTo(beNil());
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
});
|
||||
|
||||
it(@"should copy", ^{
|
||||
SQRLShipItRequest *requestCopy = [request copy];
|
||||
expect(requestCopy).to(equal(request));
|
||||
expect(requestCopy).notTo(beIdenticalTo(request));
|
||||
});
|
||||
|
||||
it(@"should fail to read state when no file exists yet", ^{
|
||||
NSError *error;
|
||||
BOOL success = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error).notTo(beNil());
|
||||
});
|
||||
|
||||
it(@"should write and read state to disk", ^{
|
||||
NSError *error;
|
||||
BOOL success = [[request writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
|
||||
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
|
||||
expect(@(success)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
|
||||
expect(readRequest).to(equal(request));
|
||||
});
|
||||
|
||||
it(@"should fail gracefully with archives encoding a different class", ^{
|
||||
NSURL *archiveLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"archive"];
|
||||
|
||||
NSError *error;
|
||||
BOOL write = [[NSKeyedArchiver archivedDataWithRootObject:@"rogue object"] writeToURL:archiveLocation atomically:YES];
|
||||
expect(@(write)).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
|
||||
BOOL success = NO;
|
||||
SQRLShipItRequest *request = [[SQRLShipItRequest readUsingURL:[RACSignal return:archiveLocation]] firstOrDefault:nil success:&success error:&error];
|
||||
expect(request).to(beNil());
|
||||
expect(@(success)).to(beFalsy());
|
||||
expect(error.domain).to(equal(SQRLShipItRequestErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(SQRLShipItRequestErrorUnarchiving)));
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// SQRLShipItStateSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-10-09.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItState.h"
|
||||
|
||||
SpecBegin(SQRLShipItState)
|
||||
|
||||
__block SQRLDirectoryManager *directoryManager;
|
||||
__block SQRLShipItState *state;
|
||||
|
||||
beforeEach(^{
|
||||
directoryManager = SQRLDirectoryManager.currentApplicationManager;
|
||||
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
|
||||
expect(state).notTo.beNil();
|
||||
|
||||
expect(state.targetBundleURL).to.equal(self.testApplicationURL);
|
||||
expect(state.updateBundleURL).to.equal(updateURL);
|
||||
expect(state.bundleIdentifier).to.beNil();
|
||||
expect(state.codeSignature).to.equal(self.testApplicationSignature);
|
||||
});
|
||||
|
||||
afterEach(^{
|
||||
NSURL *stateURL = [[directoryManager shipItStateURL] firstOrDefault:nil success:NULL error:NULL];
|
||||
expect(stateURL).notTo.beNil();
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
});
|
||||
|
||||
it(@"should copy", ^{
|
||||
SQRLShipItState *stateCopy = [state copy];
|
||||
expect(stateCopy).to.equal(state);
|
||||
expect(stateCopy).notTo.beIdenticalTo(state);
|
||||
});
|
||||
|
||||
it(@"should fail to read state when no file exists yet", ^{
|
||||
NSError *error = nil;
|
||||
BOOL success = [[SQRLShipItState readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beFalsy();
|
||||
expect(error).notTo.beNil();
|
||||
});
|
||||
|
||||
it(@"should write and read state", ^{
|
||||
NSError *error = nil;
|
||||
BOOL success = [[state writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
SQRLShipItState *readState = [[SQRLShipItState readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
|
||||
expect(success).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
expect(readState).to.equal(state);
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -6,22 +6,15 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
#import "SQRLTerminationListener.h"
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
|
||||
QuickSpecBegin(SQRLTerminationListenerSpec)
|
||||
SpecBegin(SQRLTerminationListener)
|
||||
|
||||
__block SQRLTerminationListener *listener;
|
||||
|
||||
beforeEach(^{
|
||||
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
|
||||
expect(listener).notTo(beNil());
|
||||
expect(listener).notTo.beNil();
|
||||
});
|
||||
|
||||
it(@"should complete immediately when the app is not running", ^{
|
||||
@@ -30,12 +23,10 @@ it(@"should complete immediately when the app is not running", ^{
|
||||
completed = YES;
|
||||
}];
|
||||
|
||||
expect(@(completed)).to(beTruthy());
|
||||
expect(completed).to.beTruthy();
|
||||
});
|
||||
|
||||
it(@"should wait until one instance terminates", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
|
||||
NSRunningApplication *app = [self launchTestApplicationWithEnvironment:nil];
|
||||
|
||||
__block NSRunningApplication *observedApp = nil;
|
||||
@@ -46,17 +37,15 @@ it(@"should wait until one instance terminates", ^{
|
||||
completed = YES;
|
||||
}];
|
||||
|
||||
expect(observedApp).toEventually(equal(app));
|
||||
expect(@(completed)).to(beFalsy());
|
||||
expect(observedApp).will.equal(app);
|
||||
expect(completed).to.beFalsy();
|
||||
|
||||
[app forceTerminate];
|
||||
expect(@(app.terminated)).toEventually(beTruthy());
|
||||
expect(@(completed)).toEventually(beTruthy());
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(completed).will.beTruthy();
|
||||
});
|
||||
|
||||
it(@"should wait until multiple instances terminate", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
|
||||
NSRunningApplication *app1 = [self launchTestApplicationWithEnvironment:nil];
|
||||
NSRunningApplication *app2 = [self launchTestApplicationWithEnvironment:nil];
|
||||
|
||||
@@ -65,15 +54,15 @@ it(@"should wait until multiple instances terminate", ^{
|
||||
completed = YES;
|
||||
}];
|
||||
|
||||
expect(@(completed)).to(beFalsy());
|
||||
expect(completed).to.beFalsy();
|
||||
|
||||
[app1 forceTerminate];
|
||||
expect(@(app1.terminated)).toEventually(beTruthy());
|
||||
expect(@(completed)).to(beFalsy());
|
||||
expect(app1.terminated).will.beTruthy();
|
||||
expect(completed).to.beFalsy();
|
||||
|
||||
[app2 forceTerminate];
|
||||
expect(@(app2.terminated)).toEventually(beTruthy());
|
||||
expect(@(completed)).toEventually(beTruthy());
|
||||
expect(app2.terminated).will.beTruthy();
|
||||
expect(completed).will.beTruthy();
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
//
|
||||
// QuickSpec+SQRLFixtures.h
|
||||
// SQRLTestCase.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-08-06.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
/// See https://github.com/Squirrel/Squirrel.Mac/pull/154#issuecomment-151287744
|
||||
#define SKIP_IF_RUNNING_ON_TRAVIS if (self.runningOnTravis) return;
|
||||
|
||||
// The short version string for the `testApplicationBundle`.
|
||||
extern NSString * const SQRLTestApplicationOriginalShortVersionString;
|
||||
|
||||
@@ -24,15 +16,10 @@ extern NSString * const SQRLTestApplicationUpdatedShortVersionString;
|
||||
// The Info.plist key associated with a bundle's short version string.
|
||||
extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
|
||||
/// The longer timeout we should use for things that take a long time. This is
|
||||
/// especially needed on slower machines (e.g., Travis CI).
|
||||
extern const NSTimeInterval SQRLLongTimeout;
|
||||
|
||||
@class SQRLCodeSignature;
|
||||
@class SQRLDirectoryManager;
|
||||
@class SQRLShipItRequest;
|
||||
|
||||
@interface QuickSpec (SQRLFixtures)
|
||||
@interface SQRLTestCase : SPTSenTestCase
|
||||
|
||||
// A URL to a temporary directory tests can use.
|
||||
//
|
||||
@@ -58,12 +45,13 @@ extern const NSTimeInterval SQRLLongTimeout;
|
||||
// A code signature with requirements from TestApplication.app.
|
||||
@property (nonatomic, strong, readonly) SQRLCodeSignature *testApplicationSignature;
|
||||
|
||||
// A serialized `SecRequirementRef` representing the requirements from
|
||||
// TestApplication.app.
|
||||
@property (nonatomic, copy, readonly) NSData *testApplicationCodeSigningRequirementData;
|
||||
|
||||
// A directory manager for finding URLs that apply to ShipIt.
|
||||
@property (nonatomic, strong, readonly) SQRLDirectoryManager *shipItDirectoryManager;
|
||||
|
||||
/// Are the tests currently being run on Travis?
|
||||
@property (nonatomic, readonly, assign, getter = isRunningOnTravis) BOOL runningOnTravis;
|
||||
|
||||
// Launches a new copy of TestApplication.app from the `testApplicationBundle`.
|
||||
//
|
||||
// Invoking this method multiple times will result in multiple running instances
|
||||
@@ -86,8 +74,8 @@ extern const NSTimeInterval SQRLLongTimeout;
|
||||
// deleted at the end of the example.
|
||||
- (NSURL *)createTestApplicationUpdate;
|
||||
|
||||
// Runs the installer in process or submits ShipIt's launchd job to start it up.
|
||||
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
|
||||
// Submits ShipIt's launchd job to start it up.
|
||||
- (void)launchShipIt;
|
||||
|
||||
// Creates a disk image, then mounts it.
|
||||
//
|
||||
@@ -1,20 +1,16 @@
|
||||
//
|
||||
// QuickSpec+SQRLFixtures.m
|
||||
// SQRLTestCase.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-08-06.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
|
||||
#import "SQRLTestCase.h"
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLInstaller.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItRequest.h"
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-retain-cycles"
|
||||
@@ -23,8 +19,6 @@ NSString * const SQRLTestApplicationOriginalShortVersionString = @"1.0";
|
||||
NSString * const SQRLTestApplicationUpdatedShortVersionString = @"2.1";
|
||||
NSString * const SQRLBundleShortVersionStringKey = @"CFBundleShortVersionString";
|
||||
|
||||
const NSTimeInterval SQRLLongTimeout = 20;
|
||||
|
||||
static void SQRLKillAllTestApplications(void) {
|
||||
// Forcibly kill all copies of the TestApplication that may be running.
|
||||
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"];
|
||||
@@ -45,98 +39,138 @@ 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:(RACSignal *)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) {
|
||||
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
|
||||
return @[
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
|
||||
].rac_sequence;
|
||||
}];
|
||||
RACSignal *logLocations = [[[[folders.rac_signal
|
||||
flattenMap:^(NSString *folder) {
|
||||
return [folder stringsByAppendingPaths:@[
|
||||
@"ShipIt_stdout.log",
|
||||
@"ShipIt_stderr.log",
|
||||
]].rac_signal;
|
||||
}]
|
||||
map:^(NSString *path) {
|
||||
return [appSupportURL URLByAppendingPathComponent:path];
|
||||
}]
|
||||
doNext:^(NSURL *location) {
|
||||
[[NSData data] writeToURL:location atomically:YES];
|
||||
}]
|
||||
map:^(NSURL *location) {
|
||||
return location.path;
|
||||
}];
|
||||
|
||||
[configuration beforeSuite:^{
|
||||
signal(SIGILL, &SQRLSignalHandler);
|
||||
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
|
||||
NSTask *readShipIt = [self tailTaskWithPaths:logLocations];
|
||||
[readShipIt launch];
|
||||
|
||||
cleanupBlocks = [NSMutableArray array];
|
||||
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
// We want to run any enqueued cleanup blocks after _and before_ each spec,
|
||||
// in case beforeSuites (for example) use the fixtures.
|
||||
void (^runCleanupBlocks)(void) = ^{
|
||||
// Enumerate backwards, so later resources are cleaned up first.
|
||||
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
|
||||
block();
|
||||
}
|
||||
|
||||
[cleanupBlocks removeAllObjects];
|
||||
};
|
||||
|
||||
[configuration beforeEach:^{
|
||||
runCleanupBlocks();
|
||||
}];
|
||||
|
||||
[configuration afterEach:^{
|
||||
runCleanupBlocks();
|
||||
SQRLKillAllTestApplications();
|
||||
}];
|
||||
atexit_b(^{
|
||||
[readShipIt terminate];
|
||||
});
|
||||
}
|
||||
|
||||
QuickConfigurationEnd
|
||||
- (void)setUp {
|
||||
[super setUp];
|
||||
|
||||
@implementation QuickSpec (SQRLFixtures)
|
||||
signal(SIGILL, &SQRLSignalHandler);
|
||||
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
|
||||
|
||||
#pragma mark Lifecycle
|
||||
Expecta.asynchronousTestTimeout = 3;
|
||||
}
|
||||
|
||||
- (void)SPT_setUp {
|
||||
_exampleCleanupBlocks = [[NSMutableArray alloc] init];
|
||||
}
|
||||
|
||||
- (void)SPT_tearDown {
|
||||
NSArray *cleanupBlocks = [self.exampleCleanupBlocks copy];
|
||||
_exampleCleanupBlocks = nil;
|
||||
|
||||
// Enumerate backwards, so later resources are cleaned up first.
|
||||
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
|
||||
block();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tearDown {
|
||||
[super tearDown];
|
||||
|
||||
SQRLKillAllTestApplications();
|
||||
}
|
||||
|
||||
- (void)addCleanupBlock:(dispatch_block_t)block {
|
||||
[cleanupBlocks addObject:[block copy]];
|
||||
[self.exampleCleanupBlocks addObject:[block copy]];
|
||||
}
|
||||
|
||||
#pragma mark Logging
|
||||
|
||||
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths {
|
||||
NSPipe *outputPipe = [NSPipe pipe];
|
||||
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
|
||||
|
||||
outputHandle.readabilityHandler = ^(NSFileHandle *handle) {
|
||||
NSString *output = [[NSString alloc] initWithData:handle.availableData encoding:NSUTF8StringEncoding];
|
||||
NSLog(@"\n%@", output);
|
||||
};
|
||||
|
||||
NSTask *task = [[NSTask alloc] init];
|
||||
task.launchPath = @"/usr/bin/tail";
|
||||
task.standardOutput = outputPipe;
|
||||
task.arguments = [[paths startWith:@"-f"] array];
|
||||
return task;
|
||||
}
|
||||
|
||||
#pragma mark Temporary Directory
|
||||
|
||||
- (NSURL *)baseTemporaryDirectoryURL {
|
||||
if (baseTemporaryDirectoryURL == nil) {
|
||||
if (_baseTemporaryDirectoryURL == nil) {
|
||||
NSURL *globalTemporaryDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
|
||||
baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
|
||||
|
||||
_baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", baseTemporaryDirectoryURL, error);
|
||||
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:_baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:baseTemporaryDirectoryURL error:NULL];
|
||||
baseTemporaryDirectoryURL = nil;
|
||||
[NSFileManager.defaultManager removeItemAtURL:_baseTemporaryDirectoryURL error:NULL];
|
||||
_baseTemporaryDirectoryURL = nil;
|
||||
}];
|
||||
}
|
||||
|
||||
return baseTemporaryDirectoryURL;
|
||||
return _baseTemporaryDirectoryURL;
|
||||
}
|
||||
|
||||
- (NSURL *)temporaryDirectoryURL {
|
||||
@@ -144,7 +178,7 @@ QuickConfigurationEnd
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
|
||||
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
|
||||
|
||||
return temporaryDirectoryURL;
|
||||
}
|
||||
@@ -154,15 +188,24 @@ QuickConfigurationEnd
|
||||
- (NSURL *)testApplicationURL {
|
||||
NSURL *fixtureURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app" isDirectory:YES];
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:fixtureURL.path]) {
|
||||
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL success = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:fixtureURL error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
|
||||
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
|
||||
|
||||
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
|
||||
[[NSData data] writeToURL:testAppLog atomically:YES];
|
||||
|
||||
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSignal return:testAppLog.path]];
|
||||
[readTestApp launch];
|
||||
|
||||
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[readTestApp terminate];
|
||||
}];
|
||||
}
|
||||
|
||||
return fixtureURL;
|
||||
@@ -171,8 +214,8 @@ QuickConfigurationEnd
|
||||
- (NSBundle *)testApplicationBundle {
|
||||
NSURL *fixtureURL = self.testApplicationURL;
|
||||
NSBundle *bundle = [NSBundle bundleWithURL:fixtureURL];
|
||||
XCTAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
|
||||
|
||||
STAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@@ -191,7 +234,7 @@ QuickConfigurationEnd
|
||||
|
||||
NSError *error = nil;
|
||||
NSRunningApplication *app = [NSWorkspace.sharedWorkspace launchApplicationAtURL:self.testApplicationURL options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchAndHide configuration:configuration error:&error];
|
||||
XCTAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
|
||||
STAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
if (!app.terminated) {
|
||||
@@ -211,12 +254,12 @@ QuickConfigurationEnd
|
||||
}
|
||||
|
||||
- (NSURL *)createTestApplicationUpdate {
|
||||
NSURL *originalURL = [SQRLTestBundle() URLForResource:@"TestApplication 2.1" withExtension:@"app"];
|
||||
XCTAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
|
||||
NSURL *originalURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication 2.1" withExtension:@"app"];
|
||||
STAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
|
||||
|
||||
NSError *error = nil;
|
||||
NSURL *updateParentURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:self.baseTemporaryDirectoryURL create:YES error:&error];
|
||||
XCTAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
|
||||
STAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:updateParentURL error:NULL];
|
||||
@@ -224,18 +267,18 @@ QuickConfigurationEnd
|
||||
|
||||
NSURL *updateURL = [updateParentURL URLByAppendingPathComponent:originalURL.lastPathComponent isDirectory:YES];
|
||||
BOOL success = [NSFileManager.defaultManager copyItemAtURL:originalURL toURL:updateURL error:&error];
|
||||
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
|
||||
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
|
||||
|
||||
return updateURL;
|
||||
}
|
||||
|
||||
- (id)performWithTestApplicationRequirement:(id (^)(SecRequirementRef requirement))block {
|
||||
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
|
||||
SecStaticCodeRef staticCode = NULL;
|
||||
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
|
||||
XCTAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
|
||||
STAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
|
||||
|
||||
@onExit {
|
||||
if (staticCode != NULL) CFRelease(staticCode);
|
||||
@@ -243,7 +286,7 @@ QuickConfigurationEnd
|
||||
|
||||
SecRequirementRef requirement = NULL;
|
||||
status = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &requirement);
|
||||
XCTAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
|
||||
STAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
|
||||
|
||||
@onExit {
|
||||
if (requirement != NULL) CFRelease(requirement);
|
||||
@@ -253,57 +296,48 @@ QuickConfigurationEnd
|
||||
}
|
||||
|
||||
- (SQRLCodeSignature *)testApplicationSignature {
|
||||
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
|
||||
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
|
||||
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
|
||||
return [[SQRLCodeSignature alloc] initWithRequirement:requirement];
|
||||
}];
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
SQRLCodeSignature *signature = [SQRLCodeSignature signatureWithBundle:bundleURL error:&error];
|
||||
XCTAssertNotNil(signature, @"Error getting signature for bundle at %@: %@", bundleURL, error);
|
||||
- (NSData *)testApplicationCodeSigningRequirementData {
|
||||
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
|
||||
CFDataRef data = NULL;
|
||||
OSStatus status = SecRequirementCopyData(requirement, kSecCSDefaultFlags, &data);
|
||||
STAssertTrue(status == noErr, @"Error copying data for requirement %@", requirement);
|
||||
|
||||
return signature;
|
||||
return CFBridgingRelease(data);
|
||||
}];
|
||||
}
|
||||
|
||||
- (SQRLDirectoryManager *)shipItDirectoryManager {
|
||||
NSString *identifier = SQRLShipItLauncher.shipItJobLabel;
|
||||
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:identifier];
|
||||
XCTAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
|
||||
STAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
|
||||
|
||||
return manager;
|
||||
}
|
||||
|
||||
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote {
|
||||
if (remote) {
|
||||
expect(@([[request writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL])).to(beTruthy());
|
||||
- (void)launchShipIt {
|
||||
NSError *error = nil;
|
||||
STAssertTrue([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error], @"Could not launch ShipIt: %@", error);
|
||||
|
||||
__block NSError *error = nil;
|
||||
expect(@([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error])).to(beTruthy());
|
||||
expect(error).to(beNil());
|
||||
[self addCleanupBlock:^{
|
||||
// Remove ShipIt's launchd job so it doesn't relaunch itself.
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
// Remove ShipIt's launchd job so it doesn't relaunch itself.
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
|
||||
|
||||
NSError *lookupError;
|
||||
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
|
||||
expect(stateURL).notTo(beNil());
|
||||
expect(lookupError).to(beNil());
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
}];
|
||||
} else {
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:self.shipItDirectoryManager.applicationIdentifier];
|
||||
expect(installer).notTo(beNil());
|
||||
|
||||
NSError *installedError = nil;
|
||||
BOOL installed = [[installer.installUpdateCommand execute:request] asynchronouslyWaitUntilCompleted:&installedError];
|
||||
expect(@(installed)).to(beTruthy());
|
||||
expect(installedError).to(beNil());
|
||||
}
|
||||
NSError *lookupError = nil;
|
||||
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
|
||||
STAssertNotNil(stateURL, @"Could not find state URL from %@: %@", self.shipItDirectoryManager, lookupError);
|
||||
|
||||
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL {
|
||||
NSURL *destinationURL = [self.baseTemporaryDirectoryURL URLByAppendingPathComponent:name];
|
||||
XCTAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
|
||||
STAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
|
||||
|
||||
NSString *createInvocation;
|
||||
if (directoryURL == nil) {
|
||||
@@ -312,24 +346,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
|
||||
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// SQRLURLConnectionSpec.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 27/09/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLDownloadManager.h"
|
||||
#import "SQRLResumableDownload.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import "NSError+SQRLVerbosityExtensions.h"
|
||||
#import "SQRLHTTPServer.h"
|
||||
|
||||
SpecBegin(SQRLURLConnection);
|
||||
|
||||
__block SQRLDownloadManager *downloadManager;
|
||||
|
||||
beforeAll(^{
|
||||
downloadManager = SQRLDownloadManager.defaultDownloadManager;
|
||||
|
||||
NSError *removeError = nil;
|
||||
BOOL remove = [[downloadManager
|
||||
removeAllResumableDownloads]
|
||||
waitUntilCompleted:&removeError];
|
||||
expect(remove).to.beTruthy();
|
||||
expect(removeError).to.beNil();
|
||||
});
|
||||
|
||||
beforeEach(^{
|
||||
Expecta.asynchronousTestTimeout = 60.;
|
||||
});
|
||||
|
||||
afterAll(^{
|
||||
Expecta.asynchronousTestTimeout = 1.;
|
||||
});
|
||||
|
||||
describe(@"file scheme URLs", ^{
|
||||
NSURL *fileLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"test"];
|
||||
NSData *testContents = [@"test" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSError *error;
|
||||
BOOL write = [testContents writeToURL:fileLocation options:0 error:&error];
|
||||
expect(write).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:fileLocation];
|
||||
|
||||
__block SQRLURLConnection *connection;
|
||||
|
||||
beforeEach(^{
|
||||
connection = [[SQRLURLConnection alloc] initWithRequest:request];
|
||||
});
|
||||
|
||||
void (^testBodyEqualTestContents)(NSData *) = ^ (NSData *body) {
|
||||
expect(body.length).to.equal(testContents.length);
|
||||
expect(body).to.equal(testContents);
|
||||
};
|
||||
|
||||
it(@"should retrieve file:// scheme URLs", ^{
|
||||
NSError *error;
|
||||
RACTuple *result = [[connection retrieve] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(result).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
testBodyEqualTestContents(result[1]);
|
||||
});
|
||||
|
||||
it(@"should download file:// scheme URLs", ^{
|
||||
NSError *error;
|
||||
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(result).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURL *location = result[1];
|
||||
|
||||
NSData *body = [NSData dataWithContentsOfURL:location options:0 error:&error];
|
||||
expect(error).to.beNil();
|
||||
testBodyEqualTestContents(body);
|
||||
});
|
||||
});
|
||||
|
||||
static NSData * (^stringTimes)(NSString *, NSUInteger) = ^ (NSString *string, NSUInteger times) {
|
||||
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSMutableData *data = [NSMutableData dataWithCapacity:(stringData.length + 2) * times];
|
||||
for (NSUInteger idx = 0; idx < times; idx++) {
|
||||
[data appendData:stringData];
|
||||
[data appendData:[NSData dataWithBytes:"\r\n" length:2]];
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
static SQRLHTTPServer * (^newHttpServer)(SQRLTestCase *) = ^ (SQRLTestCase *self) {
|
||||
SQRLHTTPServer *server = [[SQRLHTTPServer alloc] init];
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
[server invalidate];
|
||||
}];
|
||||
|
||||
return server;
|
||||
};
|
||||
|
||||
it(@"should resume a download", ^{
|
||||
// Start server
|
||||
|
||||
SQRLHTTPServer *server = newHttpServer(self);
|
||||
|
||||
NSError *error = nil;
|
||||
NSURL *baseURL = [server start:&error];
|
||||
expect(baseURL).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
error = nil;
|
||||
|
||||
|
||||
// Prepare half response
|
||||
|
||||
NSData *firstHalf = stringTimes(@"the quick brown fox", 100);
|
||||
NSData *secondHalf = stringTimes(@"jumped over the lazy doge", 100);
|
||||
|
||||
NSString *ETag = NSProcessInfo.processInfo.globallyUniqueString;
|
||||
|
||||
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
|
||||
NSDictionary *responseHeaders = @{
|
||||
@"Content-Length": [@(firstHalf.length + secondHalf.length) stringValue],
|
||||
@"ETag": ETag,
|
||||
};
|
||||
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
|
||||
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
|
||||
}];
|
||||
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstHalf);
|
||||
|
||||
server.responseBlock = ^ (CFHTTPMessageRef request) {
|
||||
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
|
||||
expect(ifRange).to.beNil();
|
||||
|
||||
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
|
||||
expect(range).to.beNil();
|
||||
|
||||
return serverResponse;
|
||||
};
|
||||
|
||||
|
||||
// Issue first request
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
|
||||
request.timeoutInterval = 1.;
|
||||
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
|
||||
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(result).to.beNil();
|
||||
expect(error).notTo.beNil();
|
||||
|
||||
error = nil;
|
||||
|
||||
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(download).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
|
||||
expect(downloadedData.length).to.equal(firstHalf.length);
|
||||
expect(downloadedData).to.equal(firstHalf);
|
||||
expect(error).to.beNil();
|
||||
|
||||
|
||||
// Prepare remainder response
|
||||
|
||||
serverResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 206, NULL, kCFHTTPVersion1_1);
|
||||
responseHeaders = @{
|
||||
@"Content-Length": [@(secondHalf.length) stringValue],
|
||||
@"Range": [NSString stringWithFormat:@"%lu-%lu", firstHalf.length, secondHalf.length - 1],
|
||||
@"ETag": ETag,
|
||||
};
|
||||
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
|
||||
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
|
||||
}];
|
||||
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondHalf);
|
||||
|
||||
server.responseBlock = ^ (CFHTTPMessageRef request) {
|
||||
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
|
||||
expect(ifRange).to.equal(ETag);
|
||||
|
||||
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
|
||||
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstHalf.length]));
|
||||
|
||||
return serverResponse;
|
||||
};
|
||||
|
||||
|
||||
// Issue second request
|
||||
|
||||
connection = [[SQRLURLConnection alloc] initWithRequest:request];
|
||||
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(result).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURL *location = result[1];
|
||||
|
||||
NSMutableData *fullBody = [firstHalf mutableCopy];
|
||||
[fullBody appendData:secondHalf];
|
||||
|
||||
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
|
||||
expect(downloadedData.length).to.equal(fullBody.length);
|
||||
expect(downloadedData).to.equal(fullBody);
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
|
||||
it(@"should not resume downloads for a response with a different ETag", ^{
|
||||
// Start server
|
||||
|
||||
SQRLHTTPServer *server = newHttpServer(self);
|
||||
|
||||
NSError *error = nil;
|
||||
NSURL *baseURL = [server start:&error];
|
||||
expect(baseURL).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
error = nil;
|
||||
|
||||
|
||||
// Prepare first response
|
||||
|
||||
NSData *firstBody = stringTimes(@"the quick brown fox", 100);
|
||||
NSString *firstETag = NSProcessInfo.processInfo.globallyUniqueString;
|
||||
|
||||
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
|
||||
NSDictionary *responseHeaders = @{
|
||||
// Claim the response to be longer so that the connection will timeout
|
||||
// waiting for the extra bytes
|
||||
@"Content-Length": [@(firstBody.length + 100) stringValue],
|
||||
@"ETag": firstETag,
|
||||
};
|
||||
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
|
||||
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
|
||||
}];
|
||||
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstBody);
|
||||
|
||||
server.responseBlock = ^ (CFHTTPMessageRef request) {
|
||||
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
|
||||
expect(ifRange).to.beNil();
|
||||
|
||||
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
|
||||
expect(range).to.beNil();
|
||||
|
||||
return serverResponse;
|
||||
};
|
||||
|
||||
|
||||
// Issue first request
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
|
||||
request.timeoutInterval = 1.;
|
||||
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
|
||||
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(result).to.beNil();
|
||||
expect(error).notTo.beNil();
|
||||
|
||||
error = nil;
|
||||
|
||||
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(download).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
|
||||
expect(downloadedData.length).to.equal(firstBody.length);
|
||||
expect(downloadedData).to.equal(firstBody);
|
||||
expect(error).to.beNil();
|
||||
|
||||
|
||||
// Prepare second response
|
||||
|
||||
NSData *secondBody = stringTimes(@"jumps over the lazy doge", 100);
|
||||
NSString *secondETag = NSProcessInfo.processInfo.globallyUniqueString;
|
||||
|
||||
serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
|
||||
responseHeaders = @{
|
||||
@"Content-Length": [@(secondBody.length) stringValue],
|
||||
@"ETag": secondETag,
|
||||
};
|
||||
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
|
||||
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
|
||||
}];
|
||||
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondBody);
|
||||
|
||||
server.responseBlock = ^ (CFHTTPMessageRef request) {
|
||||
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
|
||||
expect(ifRange).to.equal(firstETag);
|
||||
|
||||
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
|
||||
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstBody.length]));
|
||||
|
||||
return serverResponse;
|
||||
};
|
||||
|
||||
|
||||
// Issue second request
|
||||
|
||||
connection = [[SQRLURLConnection alloc] initWithRequest:request];
|
||||
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(result).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURL *location = result[1];
|
||||
|
||||
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
|
||||
expect(downloadedData.length).to.equal(secondBody.length);
|
||||
expect(downloadedData).to.equal(secondBody);
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
@@ -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,24 @@ 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]));
|
||||
NSError *error = nil;
|
||||
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:&error];
|
||||
expect(update.releaseDate).notTo.beNil();
|
||||
expect(error).to.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]));
|
||||
NSError *error = nil;
|
||||
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:&error];
|
||||
expect(update.releaseDate).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
+157
-200
@@ -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 "TestAppConstants.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "OHHTTPStubs/OHHTTPStubs.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLDownloadManager.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;
|
||||
};
|
||||
@@ -59,150 +53,96 @@ beforeEach(^{
|
||||
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
|
||||
});
|
||||
|
||||
describe(@"updating", ^{
|
||||
__block NSURL *updateURL;
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
|
||||
beforeEach(^{
|
||||
updateURL = [self createTestApplicationUpdate];
|
||||
});
|
||||
NSError *error = nil;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:&error];
|
||||
expect(update).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
writeUpdate(update);
|
||||
|
||||
NSError *error = nil;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:&error];
|
||||
expect(update).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
writeUpdate(update);
|
||||
it(@"should not install a corrupt update", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
|
||||
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
});
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
it(@"should not install a corrupt update", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
writeUpdate(update);
|
||||
|
||||
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
|
||||
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
// Give the update some time to finish installing.
|
||||
[NSThread sleepForTimeInterval:0.2];
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
writeUpdate(update);
|
||||
it(@"should update to the most recently enqueued job", ^{
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(self.testApplicationURL)
|
||||
} error:NULL];
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
|
||||
writeUpdate(update);
|
||||
|
||||
// Give the update some time to finish installing.
|
||||
[NSThread sleepForTimeInterval:0.2];
|
||||
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
|
||||
});
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
|
||||
it(@"should update to the most recently enqueued job", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
// Now that Test Application is launched, it's going to keep checking the
|
||||
// JSON URL until it has the proper release name. So we'll wait a short bit,
|
||||
// and then add the correct name in.
|
||||
//
|
||||
// This exercises ShipIt's ability to discard previous commands and
|
||||
// install an even newer update.
|
||||
[NSThread sleepForTimeInterval:0.3];
|
||||
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(self.testApplicationURL)
|
||||
} error:NULL];
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
// Now that Test Application is launched, it's going to keep checking the
|
||||
// JSON URL until it has the proper release name. So we'll wait a short bit,
|
||||
// and then add the correct name in.
|
||||
//
|
||||
// This exercises ShipIt's ability to discard previous commands and
|
||||
// install an even newer update.
|
||||
[NSThread sleepForTimeInterval:0.3];
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
writeUpdate(update);
|
||||
|
||||
writeUpdate(update);
|
||||
NSTimeInterval delay = 30;
|
||||
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
|
||||
|
||||
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
|
||||
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
|
||||
});
|
||||
Expecta.asynchronousTestTimeout = delay + 3;
|
||||
expect(app.terminated).will.beTruthy();
|
||||
|
||||
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
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSTimeInterval delay = 15;
|
||||
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
|
||||
|
||||
expect(@(app.terminated)).withTimeout(delay + SQRLLongTimeout).toEventually(beTruthy());
|
||||
expect(self.testApplicationBundleVersion).toEventually(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());
|
||||
|
||||
updateDirectoryURLs = [[RACSignal
|
||||
defer:^{
|
||||
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
|
||||
if (contents == nil) return [RACSignal empty];
|
||||
|
||||
return contents.rac_sequence.signal;
|
||||
}]
|
||||
filter:^(NSURL *directoryURL) {
|
||||
return [directoryURL.lastPathComponent hasPrefix:@"update."];
|
||||
}];
|
||||
});
|
||||
|
||||
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());
|
||||
|
||||
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));
|
||||
|
||||
expect([updateDirectoryURLs toArray]).toEventually(equal(@[]));
|
||||
});
|
||||
});
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
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];
|
||||
});
|
||||
@@ -218,10 +158,10 @@ describe(@"response handling", ^{
|
||||
}];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(@(result)).to(beFalsy());
|
||||
expect(error.domain).to(equal(SQRLUpdaterErrorDomain));
|
||||
expect(@(error.code)).to(equal(@(SQRLUpdaterErrorInvalidServerResponse)));
|
||||
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
|
||||
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", ^{
|
||||
@@ -235,73 +175,90 @@ 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)));
|
||||
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(result).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
|
||||
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
|
||||
});
|
||||
});
|
||||
|
||||
static RACSignal * (^stateNotificationListener)(void) = ^ {
|
||||
return [[[NSDistributedNotificationCenter.defaultCenter
|
||||
rac_addObserverForName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil]
|
||||
it(@"should clean up resumable downloads after a successful download", ^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication"];
|
||||
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:directoryManager];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL remove = [[downloadManager removeAllResumableDownloads] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(remove).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURL *updateApplicationBundle = [self createTestApplicationUpdate];
|
||||
|
||||
NSURL *firstDownload = zipUpdate(updateApplicationBundle);
|
||||
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:firstDownload]];
|
||||
BOOL download = [[connection download:downloadManager] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(download).to.beTruthy();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSURL *downloadDirectory = [[directoryManager downloadDirectoryURL] firstOrDefault:nil success:NULL error:&error];
|
||||
expect(downloadDirectory).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
|
||||
expect(contents.count).to.equal(1);
|
||||
|
||||
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateApplicationBundle),
|
||||
@"final": @YES,
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
|
||||
contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
|
||||
if (contents == nil) {
|
||||
expect(error.domain).to.equal(NSCocoaErrorDomain);
|
||||
expect(error.code).to.equal(NSFileReadNoSuchFileError);
|
||||
} else {
|
||||
expect(contents.count).to.equal(0);
|
||||
}
|
||||
});
|
||||
|
||||
it(@"should unpack update archives into a .noindex directory", ^{
|
||||
NSURL *zippedUpdateLocation = zipUpdate([self createTestApplicationUpdate]);
|
||||
|
||||
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
|
||||
@keypath(update.updateURL): zippedUpdateLocation,
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
__block SQRLDownloadedUpdate *downloadedUpdate;
|
||||
|
||||
[[[[[NSDistributedNotificationCenter.defaultCenter
|
||||
rac_addObserverForName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil]
|
||||
map:^(NSNotification *notification) {
|
||||
return notification.userInfo[SQRLTestAppUpdaterStateKey];
|
||||
return notification.userInfo[@"update"];
|
||||
}]
|
||||
setNameWithFormat:@"stateNotificationListener"];
|
||||
};
|
||||
|
||||
describe(@"state", ^{
|
||||
it(@"should transition through idle, checking and idle, when there is no update", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
|
||||
NSMutableArray *states = [NSMutableArray array];
|
||||
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
|
||||
[states addObject:state];
|
||||
tryMap:^(NSString *serialisedUpdate, NSError **errorRef) {
|
||||
NSData *JSONData = [serialisedUpdate dataUsingEncoding:NSUTF8StringEncoding];
|
||||
return [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:errorRef];
|
||||
}]
|
||||
tryMap:^(NSDictionary *JSONDictionary, NSError **errorRef) {
|
||||
return [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:errorRef];
|
||||
}]
|
||||
subscribeNext:^(SQRLDownloadedUpdate *update) {
|
||||
downloadedUpdate = update;
|
||||
}];
|
||||
|
||||
NSRunningApplication *testApplication = launchWithEnvironment(nil);
|
||||
launchWithEnvironment(nil);
|
||||
|
||||
NSArray *expectedStates = @[
|
||||
@(SQRLUpdaterStateIdle),
|
||||
@(SQRLUpdaterStateCheckingForUpdate),
|
||||
@(SQRLUpdaterStateIdle),
|
||||
];
|
||||
expect(states).toEventually(equal(expectedStates));
|
||||
expect(downloadedUpdate).willNot.beNil();
|
||||
|
||||
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
|
||||
});
|
||||
|
||||
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
|
||||
SKIP_IF_RUNNING_ON_TRAVIS
|
||||
|
||||
NSMutableArray *states = [NSMutableArray array];
|
||||
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
|
||||
[states addObject:state];
|
||||
}];
|
||||
|
||||
NSError *error;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate([self createTestApplicationUpdate]),
|
||||
@"final": @YES,
|
||||
} error:&error];
|
||||
expect(update).notTo(beNil());
|
||||
expect(error).to(beNil());
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *testApplication = launchWithEnvironment(nil);
|
||||
|
||||
NSArray *expectedStates = @[
|
||||
@(SQRLUpdaterStateIdle),
|
||||
@(SQRLUpdaterStateCheckingForUpdate),
|
||||
@(SQRLUpdaterStateDownloadingUpdate),
|
||||
@(SQRLUpdaterStateAwaitingRelaunch),
|
||||
];
|
||||
expect(states).withTimeout(SQRLLongTimeout).toEventually(equal(expectedStates));
|
||||
|
||||
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
|
||||
});
|
||||
NSURL *bundleURL = downloadedUpdate.bundle.bundleURL;
|
||||
expect([bundleURL.path rangeOfString:@".noindex"].location).notTo.equal(NSNotFound);
|
||||
});
|
||||
|
||||
QuickSpecEnd
|
||||
SpecEnd
|
||||
|
||||
@@ -6,44 +6,38 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Nimble/Nimble.h>
|
||||
#import <Quick/Quick.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
#import "QuickSpec+SQRLFixtures.h"
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
|
||||
QuickSpecBegin(SQRLZipArchiverSpec)
|
||||
SpecBegin(SQRLZipOperation)
|
||||
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// SquirrelTests-Prefix.pch
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-07-21.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#ifdef __OBJC__
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <SenTestingKit/SenTestingKit.h>
|
||||
|
||||
#import <Mantle/Mantle.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <Squirrel/Squirrel.h>
|
||||
|
||||
#define SPT_SUBCLASS SQRLTestCase
|
||||
#import "Specta.h"
|
||||
|
||||
#define EXP_SHORTHAND
|
||||
#import "Expecta.h"
|
||||
|
||||
#import "SQRLTestCase.h"
|
||||
#endif
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Headers
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Mantle
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Modules
|
||||
@@ -1 +0,0 @@
|
||||
Versions/Current/Resources
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
//
|
||||
// MTLJSONAdapter.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-02-12.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class MTLModel;
|
||||
|
||||
// A MTLModel object that supports being parsed from and serialized to JSON.
|
||||
@protocol MTLJSONSerializing
|
||||
@required
|
||||
|
||||
// Specifies how to map property keys to different key paths in JSON.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Any property keys not present in the dictionary are assumed to match the JSON
|
||||
// key that should be used. Any keys associated with NSNull will not participate
|
||||
// in JSON serialization.
|
||||
//
|
||||
// Returns a dictionary mapping property keys to JSON key paths (as strings) or
|
||||
// NSNull values.
|
||||
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
|
||||
|
||||
@optional
|
||||
|
||||
// Specifies how to convert a JSON value to the given property key. If
|
||||
// reversible, the transformer will also be used to convert the property value
|
||||
// back to JSON.
|
||||
//
|
||||
// If the receiver implements a `+<key>JSONTransformer` method, MTLJSONAdapter
|
||||
// will use the result of that method instead.
|
||||
//
|
||||
// Returns a value transformer, or nil if no transformation should be performed.
|
||||
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
|
||||
|
||||
// Overridden to parse the receiver as a different class, based on information
|
||||
// in the provided dictionary.
|
||||
//
|
||||
// This is mostly useful for class clusters, where the abstract base class would
|
||||
// be passed into -[MTLJSONAdapter initWithJSONDictionary:modelClass:], but
|
||||
// a subclass should be instantiated instead.
|
||||
//
|
||||
// JSONDictionary - The JSON dictionary that will be parsed.
|
||||
//
|
||||
// Returns the class that should be parsed (which may be the receiver), or nil
|
||||
// to abort parsing (e.g., if the data is invalid).
|
||||
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
|
||||
|
||||
@end
|
||||
|
||||
// The domain for errors originating from MTLJSONAdapter.
|
||||
extern NSString * const MTLJSONAdapterErrorDomain;
|
||||
|
||||
// +classForParsingJSONDictionary: returned nil for the given dictionary.
|
||||
extern const NSInteger MTLJSONAdapterErrorNoClassFound;
|
||||
|
||||
// The provided JSONDictionary is not valid.
|
||||
extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;
|
||||
|
||||
// The model's implementation of +JSONKeyPathsByPropertyKey included a key which
|
||||
// does not actually exist in +propertyKeys.
|
||||
extern const NSInteger MTLJSONAdapterErrorInvalidJSONMapping;
|
||||
|
||||
// Converts a MTLModel object to and from a JSON dictionary.
|
||||
@interface MTLJSONAdapter : NSObject
|
||||
|
||||
// The model object that the receiver was initialized with, or that the receiver
|
||||
// parsed from a JSON dictionary.
|
||||
@property (nonatomic, strong, readonly) MTLModel<MTLJSONSerializing> *model;
|
||||
|
||||
// Attempts to parse a JSON dictionary into a model object.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to attempt to parse from the JSON.
|
||||
// This class must conform to <MTLJSONSerializing>. This
|
||||
// argument must not be nil.
|
||||
// JSONDictionary - A dictionary representing JSON data. This should match the
|
||||
// format returned by NSJSONSerialization. If this argument is
|
||||
// nil, the method returns nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// parsing or initializing an instance of `modelClass`.
|
||||
//
|
||||
// Returns an instance of `modelClass` upon success, or nil if a parsing error
|
||||
// occurred.
|
||||
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
|
||||
|
||||
// Attempts to parse an array of JSON dictionary objects into a model objects
|
||||
// of a specific class.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
|
||||
// class must conform to <MTLJSONSerializing>. This argument must
|
||||
// not be nil.
|
||||
// JSONArray - A array of dictionaries representing JSON data. This should
|
||||
// match the format returned by NSJSONSerialization. If this
|
||||
// argument is nil, the method returns nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// parsing or initializing an any of the instances of
|
||||
// `modelClass`.
|
||||
//
|
||||
// Returns an array of `modelClass` instances upon success, or nil if a parsing
|
||||
// error occurred.
|
||||
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
|
||||
|
||||
// Converts a model into a JSON representation.
|
||||
//
|
||||
// model - The model to use for JSON serialization. This argument must not be
|
||||
// nil.
|
||||
//
|
||||
// Returns a JSON dictionary, or nil if a serialization error occurred.
|
||||
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model;
|
||||
|
||||
// Converts a array of models into a JSON representation.
|
||||
//
|
||||
// models - The array of models to use for JSON serialization. This argument
|
||||
// must not be nil.
|
||||
//
|
||||
// Returns a JSON array, or nil if a serialization error occurred for any
|
||||
// model.
|
||||
+ (NSArray *)JSONArrayFromModels:(NSArray *)models;
|
||||
|
||||
// Initializes the receiver by attempting to parse a JSON dictionary into
|
||||
// a model object.
|
||||
//
|
||||
// JSONDictionary - A dictionary representing JSON data. This should match the
|
||||
// format returned by NSJSONSerialization. If this argument is
|
||||
// nil, the method returns nil and an error with code
|
||||
// MTLJSONAdapterErrorInvalidJSONDictionary.
|
||||
// modelClass - The MTLModel subclass to attempt to parse from the JSON.
|
||||
// This class must conform to <MTLJSONSerializing>. This
|
||||
// argument must not be nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// parsing or initializing an instance of `modelClass`.
|
||||
//
|
||||
// Returns an initialized adapter upon success, or nil if a parsing error
|
||||
// occurred.
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error;
|
||||
|
||||
// Initializes the receiver with an existing model.
|
||||
//
|
||||
// model - The model to use for JSON serialization. This argument must not be
|
||||
// nil.
|
||||
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model;
|
||||
|
||||
// Serializes the receiver's `model` into JSON.
|
||||
//
|
||||
// Returns a JSON dictionary, or nil if a serialization error occurred.
|
||||
- (NSDictionary *)JSONDictionary;
|
||||
|
||||
// Looks up the JSON key path in the model's +propertyKeys.
|
||||
//
|
||||
// Subclasses may override this method to customize the adapter's seralizing
|
||||
// behavior. You should not call this method directly.
|
||||
//
|
||||
// key - The property key to retrieve the corresponding JSON key path for. This
|
||||
// argument must not be nil.
|
||||
//
|
||||
// Returns a key path to use, or nil to omit the property from JSON.
|
||||
- (NSString *)JSONKeyPathForPropertyKey:(NSString *)key;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLJSONAdapter (Deprecated)
|
||||
|
||||
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary __attribute__((deprecated("Replaced by +modelOfClass:fromJSONDictionary:error:")));
|
||||
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass __attribute__((deprecated("Replaced by -initWithJSONDictionary:modelClass:error:")));
|
||||
|
||||
@end
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
//
|
||||
// MTLManagedObjectAdapter.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-03-29.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class MTLModel;
|
||||
|
||||
// A MTLModel object that supports being serialized to and from Core Data as an
|
||||
// NSManagedObject.
|
||||
@protocol MTLManagedObjectSerializing
|
||||
@required
|
||||
|
||||
// The name of the Core Data entity that the receiver serializes to and
|
||||
// deserializes from.
|
||||
//
|
||||
// This method must not return nil.
|
||||
+ (NSString *)managedObjectEntityName;
|
||||
|
||||
// Specifies how to map property keys to different keys on the receiver's
|
||||
// +managedObjectEntity.
|
||||
//
|
||||
// Entity attributes will be mapped to and from the receiver's properties using
|
||||
// +entityAttributeTransformerForKey:. Entity relationships will be mapped to
|
||||
// and from MTLModel objects using +relationshipModelClassesByPropertyKey.
|
||||
// Fetched properties are not supported.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Any property keys not present in the dictionary are assumed to match the
|
||||
// entity key that should be used. Any keys associated with NSNull will not
|
||||
// participate in managed object serialization.
|
||||
//
|
||||
// Returns a dictionary mapping property keys to entity keys (as strings) or
|
||||
// NSNull values.
|
||||
+ (NSDictionary *)managedObjectKeysByPropertyKey;
|
||||
|
||||
@optional
|
||||
|
||||
// Specifies a set of property keys used by the adapter to check for an already
|
||||
// existing managed object when converting the MTLModel to its related
|
||||
// NSManagedObject.
|
||||
//
|
||||
// The adapter will first map any keys provided by this method to the correct
|
||||
// keys in managedObjectKeysByPropertyKey.
|
||||
//
|
||||
// The adapter will then perform a fetch request in the provided context for a
|
||||
// managed object that matches the MTLModel's managedObjectEntityName and has
|
||||
// equal values set for the property keys on the MTLModel.
|
||||
//
|
||||
// The managed object returned by the fetch request will then be set with all
|
||||
// values from the MTLModel that the managed object is being converted from.
|
||||
//
|
||||
// If a property value of our MTLModel is yet another MTLModel which needs to be
|
||||
// converted to a managed object, the class for that MTLModel can also implement
|
||||
// this method to perform its own uniqing.
|
||||
//
|
||||
// For example:
|
||||
// 1. An MTLModel subclass has id_number = 10.
|
||||
// 2. An NSManagedObject accessible to the adapter's context has idnumber = 10.
|
||||
// 3. managedObjectKeysByPropertyKey returns @{@"id_number" : @"idnumber"}
|
||||
// 4. propertyKeysForManagedObjectUniquing returns
|
||||
// [NSSet setWithObject:@"id_number"];
|
||||
// 5. Then our fetch request may return this managed object (or another managed
|
||||
// object with idnumber = 10).
|
||||
//
|
||||
// NOTE: If multiple managed objects follow the same uniquing criteria only one
|
||||
// of them will be set with our MTLModel's values.
|
||||
+ (NSSet *)propertyKeysForManagedObjectUniquing;
|
||||
|
||||
// Specifies how to convert the given property key to a managed object
|
||||
// attribute. If reversible, the transformer will also be used to convert the
|
||||
// managed object attribute back to the property.
|
||||
//
|
||||
// If the receiver implements a `+<key>EntityAttributeTransformer` method,
|
||||
// MTLManagedObjectAdapter will use the result of that method instead.
|
||||
//
|
||||
// Returns a value transformer, or nil if no transformation should be performed.
|
||||
+ (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key;
|
||||
|
||||
// Specifies the MTLModel subclasses that should be deserialized to the
|
||||
// receiver's property keys when a property key corresponds to an entity
|
||||
// relationship.
|
||||
//
|
||||
// In other words, the dictionary returned by this method is used to decode
|
||||
// managed object relationships into MTLModels (or NSArrays or NSSets thereof)
|
||||
// set on the receiver.
|
||||
//
|
||||
// If a property key is omitted from the returned dictionary, but present in
|
||||
// +managedObjectKeysByPropertyKey, and the receiver's +managedObjectEntity has
|
||||
// a relationship at the corresponding key, an exception will be thrown during
|
||||
// deserialization.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Returns a dictionary mapping property keys to the Class objects that should
|
||||
// be used.
|
||||
+ (NSDictionary *)relationshipModelClassesByPropertyKey;
|
||||
|
||||
// Overridden to deserialize a different class instead of the receiver, based on
|
||||
// information in the provided object.
|
||||
//
|
||||
// This is mostly useful for class clusters, where the abstract base class would
|
||||
// be passed into +[MTLManagedObjectAdapter
|
||||
// modelOfClass:fromManagedObject:error:], but a subclass should be instantiated
|
||||
// instead.
|
||||
//
|
||||
// managedObject - The object that will be deserialized.
|
||||
//
|
||||
// Returns the class that should be instantiated (which may be the receiver), or
|
||||
// nil to abort parsing (e.g., if the data is invalid).
|
||||
+ (Class)classForDeserializingManagedObject:(NSManagedObject *)managedObject;
|
||||
|
||||
// Overriden when merging the value of the given key on the receiver with the
|
||||
// value of the same key from the given `NSManagedObject` requires custom
|
||||
// handling.
|
||||
//
|
||||
// By default, this method is not implemented, and precedence will be given to
|
||||
// the value of the receiving model implicitly.
|
||||
//
|
||||
// When implemented, this method is called when an existing `NSManagedObject`
|
||||
// is found for the receiving model, before updating the `NSManagedObject`'s
|
||||
// properties.
|
||||
//
|
||||
// When implementing, you should use `+managedObjectKeysByPropertyKey` to map
|
||||
// the given `key` to the appropriate `NSManagedObject` property.
|
||||
- (void)mergeValueForKey:(NSString *)key fromManagedObject:(NSManagedObject *)managedObject;
|
||||
|
||||
// Overriden when merging values on the receiver with the given
|
||||
// `NSManagedObject` requires custom handling.
|
||||
//
|
||||
// By default, this method is not implemented, and precedence will be given to
|
||||
// the values of the receiving model implicitly.
|
||||
//
|
||||
// When implemented, this method is called when an existing `NSManagedObject`
|
||||
// is found for the receiving model, before updating the `NSManagedObject`'s
|
||||
// properties.
|
||||
//
|
||||
// When implementing, you should use `+managedObjectKeysByPropertyKey` to map
|
||||
// the given `key` to the appropriate `NSManagedObject` property.
|
||||
//
|
||||
// If you have also implemented `mergeValueForKey:fromManagedObject:` you have
|
||||
// to make sure to call `mergeValueForKey:fromManagedObject:` from this method
|
||||
// when appropriate.
|
||||
- (void)mergeValuesForKeysFromManagedObject:(NSManagedObject *)managedObject;
|
||||
|
||||
@end
|
||||
|
||||
// The domain for errors originating from MTLManagedObjectAdapter.
|
||||
extern NSString * const MTLManagedObjectAdapterErrorDomain;
|
||||
|
||||
// +classForDeserializingManagedObject: returned nil for the given object.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorNoClassFound;
|
||||
|
||||
// An NSManagedObject failed to initialize.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorInitializationFailed;
|
||||
|
||||
// The managed object key specified by +managedObjectKeysByPropertyKey does not
|
||||
// exist in the NSEntityDescription.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectKey;
|
||||
|
||||
// The managed object property specified has a type that isn't supported by
|
||||
// MTLManagedObjectAdapter.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType;
|
||||
|
||||
// The fetch request to find an existing managed object based on
|
||||
// `+propertyKeysForManagedObjectUniquing` failed.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorUniqueFetchRequestFailed;
|
||||
|
||||
// A MTLModel property cannot be serialized to or deserialized from an
|
||||
// NSManagedObject relationship.
|
||||
//
|
||||
// For a to-one relationship, this means that the property does not contain
|
||||
// a MTLModel, or the MTLModel does not conform to <MTLManagedObjectSerializing>.
|
||||
//
|
||||
// For a to-many relationship, this means that the property does not contain an
|
||||
// NSArray or NSSet of MTLModel<MTLManagedObjectSerializing> instances.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedRelationshipClass;
|
||||
|
||||
// The model's implementation of +managedObjectKeysByPropertyKey included a key
|
||||
// which does not actually exist in +propertyKeys.
|
||||
extern const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectMapping;
|
||||
|
||||
// Converts a MTLModel object to and from an NSManagedObject.
|
||||
@interface MTLManagedObjectAdapter : NSObject
|
||||
|
||||
// Attempts to deserialize an NSManagedObject into a MTLModel object.
|
||||
//
|
||||
// modelClass - The MTLModel subclass to return. This class must conform to
|
||||
// <MTLManagedObjectSerializing>. This argument must not be nil.
|
||||
// managedObject - The managed object to deserialize. If this argument is nil,
|
||||
// the method returns nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// deserialization or initializing an instance of `modelClass`.
|
||||
//
|
||||
// Returns an instance of `modelClass` upon success, or nil if an error
|
||||
// occurred.
|
||||
+ (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)managedObject error:(NSError **)error;
|
||||
|
||||
// Serializes a MTLModel into an NSManagedObject.
|
||||
//
|
||||
// model - The model object to serialize. This argument must not be nil.
|
||||
// context - The context into which to insert the created managed object. This
|
||||
// argument must not be nil.
|
||||
// error - If not NULL, this may be set to an error that occurs during
|
||||
// serialization or insertion.
|
||||
+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error;
|
||||
|
||||
@end
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
//
|
||||
// MTLModel+NSCoding.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2013-02-12.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MTLModel.h"
|
||||
|
||||
// Defines how a MTLModel property key should be encoded into an archive.
|
||||
//
|
||||
// MTLModelEncodingBehaviorExcluded - The property should never be encoded.
|
||||
// MTLModelEncodingBehaviorUnconditional - The property should always be
|
||||
// encoded.
|
||||
// MTLModelEncodingBehaviorConditional - The object should be encoded only
|
||||
// if unconditionally encoded elsewhere.
|
||||
// This should only be used for object
|
||||
// properties.
|
||||
typedef enum : NSUInteger {
|
||||
MTLModelEncodingBehaviorExcluded = 0,
|
||||
MTLModelEncodingBehaviorUnconditional,
|
||||
MTLModelEncodingBehaviorConditional,
|
||||
} MTLModelEncodingBehavior;
|
||||
|
||||
// Implements default archiving and unarchiving behaviors for MTLModel.
|
||||
@interface MTLModel (NSCoding) <NSCoding>
|
||||
|
||||
// Initializes the receiver from an archive.
|
||||
//
|
||||
// This will decode the original +modelVersion of the archived object, then
|
||||
// invoke -decodeValueForKey:withCoder:modelVersion: for each of the receiver's
|
||||
// +propertyKeys.
|
||||
//
|
||||
// Returns an initialized model object, or nil if a decoding error occurred.
|
||||
- (id)initWithCoder:(NSCoder *)coder;
|
||||
|
||||
// Archives the receiver using the given coder.
|
||||
//
|
||||
// This will encode the receiver's +modelVersion, then the receiver's properties
|
||||
// according to the behaviors specified in +encodingBehaviorsByPropertyKey.
|
||||
- (void)encodeWithCoder:(NSCoder *)coder;
|
||||
|
||||
// Determines how the +propertyKeys of the class are encoded into an archive.
|
||||
// The values of this dictionary should be boxed MTLModelEncodingBehavior
|
||||
// values.
|
||||
//
|
||||
// Any keys not present in the dictionary will be excluded from the archive.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Returns a dictionary mapping the receiver's +propertyKeys to default encoding
|
||||
// behaviors. If a property is an object with `weak` semantics, the default
|
||||
// behavior is MTLModelEncodingBehaviorConditional; otherwise, the default is
|
||||
// MTLModelEncodingBehaviorUnconditional.
|
||||
+ (NSDictionary *)encodingBehaviorsByPropertyKey;
|
||||
|
||||
// Determines the classes that are allowed to be decoded for each of the
|
||||
// receiver's properties when using <NSSecureCoding>. The values of this
|
||||
// dictionary should be NSArrays of Class objects.
|
||||
//
|
||||
// If any encodable keys (as determined by +encodingBehaviorsByPropertyKey) are
|
||||
// not present in the dictionary, an exception will be thrown during secure
|
||||
// encoding or decoding.
|
||||
//
|
||||
// Subclasses overriding this method should combine their values with those of
|
||||
// `super`.
|
||||
//
|
||||
// Returns a dictionary mapping the receiver's encodable keys (as determined by
|
||||
// +encodingBehaviorsByPropertyKey) to default allowed classes, based on the
|
||||
// type that each property is declared as. If type of an encodable property
|
||||
// cannot be determined (e.g., it is declared as `id`), it will be omitted from
|
||||
// the dictionary, and subclasses must provide a valid value to prevent an
|
||||
// exception being thrown during encoding/decoding.
|
||||
+ (NSDictionary *)allowedSecureCodingClassesByPropertyKey;
|
||||
|
||||
// Decodes the value of the given property key from an archive.
|
||||
//
|
||||
// By default, this method looks for a `-decode<Key>WithCoder:modelVersion:`
|
||||
// method on the receiver, and invokes it if found.
|
||||
//
|
||||
// If the custom method is not implemented and `coder` does not require secure
|
||||
// coding, `-[NSCoder decodeObjectForKey:]` will be invoked with the given
|
||||
// `key`.
|
||||
//
|
||||
// If the custom method is not implemented and `coder` requires secure coding,
|
||||
// `-[NSCoder decodeObjectOfClasses:forKey:]` will be invoked with the
|
||||
// information from +allowedSecureCodingClassesByPropertyKey and the given `key`. The
|
||||
// receiver must conform to <NSSecureCoding> for this to work correctly.
|
||||
//
|
||||
// key - The property key to decode the value for. This argument cannot
|
||||
// be nil.
|
||||
// coder - The NSCoder representing the archive being decoded. This
|
||||
// argument cannot be nil.
|
||||
// modelVersion - The version of the original model object that was encoded.
|
||||
//
|
||||
// Returns the decoded and boxed value, or nil if the key was not present.
|
||||
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion;
|
||||
|
||||
// The version of this MTLModel subclass.
|
||||
//
|
||||
// This version number is saved in archives so that later model changes can be
|
||||
// made backwards-compatible with old versions.
|
||||
//
|
||||
// Subclasses should override this method to return a higher version number
|
||||
// whenever a breaking change is made to the model.
|
||||
//
|
||||
// Returns 0.
|
||||
+ (NSUInteger)modelVersion;
|
||||
|
||||
@end
|
||||
|
||||
// This method must be overridden to support archives created by older versions
|
||||
// of Mantle (before the `MTLModel+NSCoding` interface existed).
|
||||
@interface MTLModel (OldArchiveSupport)
|
||||
|
||||
// Converts an archived external representation to a dictionary suitable for
|
||||
// passing to -initWithDictionary:.
|
||||
//
|
||||
// externalRepresentation - The decoded external representation of the receiver.
|
||||
// fromVersion - The model version at the time the external
|
||||
// representation was encoded.
|
||||
//
|
||||
// Returns nil by default, indicating that conversion failed.
|
||||
+ (NSDictionary *)dictionaryValueFromArchivedExternalRepresentation:(NSDictionary *)externalRepresentation version:(NSUInteger)fromVersion;
|
||||
|
||||
@end
|
||||
-125
@@ -1,125 +0,0 @@
|
||||
//
|
||||
// MTLModel.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2012-09-11.
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// An abstract base class for model objects, using reflection to provide
|
||||
// sensible default behaviors.
|
||||
//
|
||||
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
|
||||
// the +propertyKeys method.
|
||||
@interface MTLModel : NSObject <NSCopying>
|
||||
|
||||
// Returns a new instance of the receiver initialized using
|
||||
// -initWithDictionary:error:.
|
||||
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
// Initializes the receiver with default values.
|
||||
//
|
||||
// This is the designated initializer for this class.
|
||||
- (instancetype)init;
|
||||
|
||||
// Initializes the receiver using key-value coding, setting the keys and values
|
||||
// in the given dictionary.
|
||||
//
|
||||
// Subclass implementations may override this method, calling the super
|
||||
// implementation, in order to perform further processing and initialization
|
||||
// after deserialization.
|
||||
//
|
||||
// dictionaryValue - Property keys and values to set on the receiver. Any NSNull
|
||||
// values will be converted to nil before being used. KVC
|
||||
// validation methods will automatically be invoked for all of
|
||||
// the properties given. If nil, this method is equivalent to
|
||||
// -init.
|
||||
// error - If not NULL, this may be set to any error that occurs
|
||||
// (like a KVC validation error).
|
||||
//
|
||||
// Returns an initialized model object, or nil if validation failed.
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
|
||||
|
||||
// Returns the keys for all @property declarations, except for `readonly`
|
||||
// properties without ivars, or properties on MTLModel itself.
|
||||
+ (NSSet *)propertyKeys;
|
||||
|
||||
// A dictionary representing the properties of the receiver.
|
||||
//
|
||||
// The default implementation combines the values corresponding to all
|
||||
// +propertyKeys into a dictionary, with any nil values represented by NSNull.
|
||||
//
|
||||
// This property must never be nil.
|
||||
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
|
||||
|
||||
// Merges the value of the given key on the receiver with the value of the same
|
||||
// key from the given model object, giving precedence to the other model object.
|
||||
//
|
||||
// By default, this method looks for a `-merge<Key>FromModel:` method on the
|
||||
// receiver, and invokes it if found. If not found, and `model` is not nil, the
|
||||
// value for the given key is taken from `model`.
|
||||
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;
|
||||
|
||||
// Merges the values of the given model object into the receiver, using
|
||||
// -mergeValueForKey:fromModel: for each key in +propertyKeys.
|
||||
//
|
||||
// `model` must be an instance of the receiver's class or a subclass thereof.
|
||||
- (void)mergeValuesForKeysFromModel:(MTLModel *)model;
|
||||
|
||||
// Compares the receiver with another object for equality.
|
||||
//
|
||||
// The default implementation is equivalent to comparing both models'
|
||||
// -dictionaryValue.
|
||||
//
|
||||
// Note that this may lead to infinite loops if the receiver holds a circular
|
||||
// reference to another MTLModel and both use the default behavior.
|
||||
// It is recommended to override -isEqual: in this scenario.
|
||||
- (BOOL)isEqual:(id)object;
|
||||
|
||||
// A string that describes the contents of the receiver.
|
||||
//
|
||||
// The default implementation is based on the receiver's class and its
|
||||
// -dictionaryValue.
|
||||
//
|
||||
// Note that this may lead to infinite loops if the receiver holds a circular
|
||||
// reference to another MTLModel and both use the default behavior.
|
||||
// It is recommended to override -description in this scenario.
|
||||
- (NSString *)description;
|
||||
|
||||
@end
|
||||
|
||||
// Implements validation logic for MTLModel.
|
||||
@interface MTLModel (Validation)
|
||||
|
||||
// Validates the model.
|
||||
//
|
||||
// The default implementation simply invokes -validateValue:forKey:error: with
|
||||
// all +propertyKeys and their current value. If -validateValue:forKey:error:
|
||||
// returns a new value, the property is set to that new value.
|
||||
//
|
||||
// error - If not NULL, this may be set to any error that occurs during
|
||||
// validation
|
||||
//
|
||||
// Returns YES if the model is valid, or NO if the validation failed.
|
||||
- (BOOL)validate:(NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
@interface MTLModel (Unavailable)
|
||||
|
||||
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue __attribute__((deprecated("Replaced by +modelWithDictionary:error:")));
|
||||
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue __attribute__((deprecated("Replaced by -initWithDictionary:error:")));
|
||||
|
||||
+ (instancetype)modelWithExternalRepresentation:(NSDictionary *)externalRepresentation __attribute__((deprecated("Replaced by -[MTLJSONAdapter initWithJSONDictionary:modelClass:]")));
|
||||
- (instancetype)initWithExternalRepresentation:(NSDictionary *)externalRepresentation __attribute__((deprecated("Replaced by -[MTLJSONAdapter initWithJSONDictionary:modelClass:]")));
|
||||
|
||||
@property (nonatomic, copy, readonly) NSDictionary *externalRepresentation __attribute__((deprecated("Replaced by MTLJSONAdapter.JSONDictionary")));
|
||||
|
||||
+ (NSDictionary *)externalRepresentationKeyPathsByPropertyKey __attribute__((deprecated("Replaced by +JSONKeyPathsByPropertyKey in <MTLJSONSerializing>")));
|
||||
+ (NSValueTransformer *)transformerForKey:(NSString *)key __attribute__((deprecated("Replaced by +JSONTransformerForKey: in <MTLJSONSerializing>")));
|
||||
|
||||
+ (NSDictionary *)migrateExternalRepresentation:(NSDictionary *)externalRepresentation fromVersion:(NSUInteger)fromVersion __attribute__((deprecated("Replaced by -decodeValueForKey:withCoder:modelVersion:")));
|
||||
|
||||
@end
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// MTLValueTransformer.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2012-09-11.
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
typedef id (^MTLValueTransformerBlock)(id);
|
||||
|
||||
//
|
||||
// A value transformer supporting block-based transformation.
|
||||
//
|
||||
@interface MTLValueTransformer : NSValueTransformer
|
||||
|
||||
// Returns a transformer which transforms values using the given block. Reverse
|
||||
// transformations will not be allowed.
|
||||
+ (instancetype)transformerWithBlock:(MTLValueTransformerBlock)transformationBlock;
|
||||
|
||||
// Returns a transformer which transforms values using the given block, for
|
||||
// forward or reverse transformations.
|
||||
+ (instancetype)reversibleTransformerWithBlock:(MTLValueTransformerBlock)transformationBlock;
|
||||
|
||||
// Returns a transformer which transforms values using the given blocks.
|
||||
+ (instancetype)reversibleTransformerWithForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock;
|
||||
|
||||
@end
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// Mantle.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Justin Spahr-Summers on 2012-09-04.
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
//! Project version number for Mantle.
|
||||
FOUNDATION_EXPORT double MantleVersionNumber;
|
||||
|
||||
//! Project version string for Mantle.
|
||||
FOUNDATION_EXPORT const unsigned char MantleVersionString[];
|
||||
|
||||
#import <Mantle/MTLJSONAdapter.h>
|
||||
#import <Mantle/MTLManagedObjectAdapter.h>
|
||||
#import <Mantle/MTLModel.h>
|
||||
#import <Mantle/MTLModel+NSCoding.h>
|
||||
#import <Mantle/MTLValueTransformer.h>
|
||||
#import <Mantle/NSArray+MTLManipulationAdditions.h>
|
||||
#import <Mantle/NSDictionary+MTLManipulationAdditions.h>
|
||||
#import <Mantle/NSObject+MTLComparisonAdditions.h>
|
||||
#import <Mantle/NSValueTransformer+MTLInversionAdditions.h>
|
||||
#import <Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.h>
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
//
|
||||
// NSArray+MTLManipulationAdditions.h
|
||||
// Mantle
|
||||
//
|
||||
// Created by Josh Abernathy on 9/19/12.
|
||||
// Copyright (c) 2012 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface NSArray (MTLManipulationAdditions)
|
||||
|
||||
// The first object in the array or nil if the array is empty.
|
||||
// Forwards to `firstObject` which has been first declared in iOS7, but works with iOS4/10.6.
|
||||
@property (nonatomic, readonly, strong) id mtl_firstObject;
|
||||
|
||||
// Returns a new array without all instances of the given object.
|
||||
- (NSArray *)mtl_arrayByRemovingObject:(id)object;
|
||||
|
||||
// Returns a new array without the first object. If the array is empty, it
|
||||
// returns the empty array.
|
||||
- (NSArray *)mtl_arrayByRemovingFirstObject;
|
||||
|
||||
// Returns a new array without the last object. If the array is empty, it
|
||||
// returns the empty array.
|
||||
- (NSArray *)mtl_arrayByRemovingLastObject;
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user