Compare commits
284
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 | ||
|
|
2388fa3475 | ||
|
|
99bb3e042f | ||
|
|
5b930bcdb8 | ||
|
|
7aace7204d | ||
|
|
c42950ccc4 | ||
|
|
efbb838e09 | ||
|
|
249481c842 | ||
|
|
4062563d09 | ||
|
|
55ca2cd87b | ||
|
|
6159fe07d5 | ||
|
|
2fed661399 | ||
|
|
1a9c430aca | ||
|
|
2220774e01 | ||
|
|
77f5c72415 | ||
|
|
3bd1e41270 | ||
|
|
9b92542b9c | ||
|
|
56b08482f4 | ||
|
|
c96467f35d | ||
|
|
707678e926 | ||
|
|
c50efc62a1 | ||
|
|
e4b7973685 | ||
|
|
e4c1a33251 | ||
|
|
7ba896fdde | ||
|
|
9fa097bec6 | ||
|
|
9f8cbe17c3 | ||
|
|
6f3e933d67 | ||
|
|
516ab2af65 | ||
|
|
4d385f4a33 | ||
|
|
d5456f90f2 | ||
|
|
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 | ||
|
|
72d768d17d | ||
|
|
91549567e1 | ||
|
|
6e9efa0be3 | ||
|
|
8709338949 | ||
|
|
f9d6c00f60 | ||
|
|
e122927c5d | ||
|
|
43d524e186 | ||
|
|
a4dc3e4a47 | ||
|
|
e5f838463b | ||
|
|
e24d538232 | ||
|
|
9170c4d106 | ||
|
|
a93cf336b8 | ||
|
|
7bc6620b45 | ||
|
|
4f0ba2c41a | ||
|
|
cfaabf7cb0 | ||
|
|
d4cd9fa88c | ||
|
|
a7a3ddbf9c | ||
|
|
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 |
@@ -4,3 +4,6 @@
|
||||
[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
|
||||
|
||||
Vendored
+1
-1
Submodule External/Mantle updated: 6cbdbebc44...8aef509d18
+1
Submodule External/OHHTTPStubs added at acaf8b8af7
Vendored
+1
-1
Submodule External/ReactiveCocoa updated: c01fe91544...0138dabe60
@@ -19,6 +19,8 @@ Support](#server-support) below.
|
||||
Squirrel’s installer is also designed to be fault tolerant, and ensure that any
|
||||
updates installed are valid.
|
||||
|
||||

|
||||
|
||||
# Adopting Squirrel
|
||||
|
||||
1. Add the Squirrel repository as a git submodule
|
||||
@@ -36,30 +38,38 @@ Once Squirrel is added to your project, you need to configure and start it.
|
||||
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
|
||||
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://mycompany.com/myapp/latest"]]];
|
||||
[self.updater startAutomaticChecksWithInterval:/* 4 Hours */ 60 * 60 * 4];
|
||||
[self.updater checkForUpdates];
|
||||
|
||||
// Check for updates every 4 hours.
|
||||
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
|
||||
}
|
||||
```
|
||||
|
||||
Squirrel will periodically request and automatically download any updates.
|
||||
Squirrel will periodically request and automatically download any updates. When
|
||||
your application terminates, any downloaded update will be automatically
|
||||
installed.
|
||||
|
||||
Before your application terminates, it should tell Squirrel to install any
|
||||
updates that it has downloaded:
|
||||
## Update Notifications
|
||||
|
||||
To know when an update is ready to be installed, you can subscribe to the
|
||||
`updates` signal on `SQRLUpdater`:
|
||||
|
||||
```objc
|
||||
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
|
||||
[self.updater installUpdateIfNeeded:^(BOOL success, NSError *error) {
|
||||
if (success) {
|
||||
NSLog(@"Successfully installed an update");
|
||||
} else {
|
||||
NSLog(@"Failed to update: %@", error);
|
||||
}
|
||||
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
|
||||
NSLog(@"An update is ready to install: %@", downloadedUpdate);
|
||||
}];
|
||||
```
|
||||
|
||||
[sender replyToApplicationShouldTerminate:YES];
|
||||
}];
|
||||
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.
|
||||
|
||||
return NSTerminateLater;
|
||||
}
|
||||
If you want to install a downloaded update and automatically relaunch afterward,
|
||||
`SQRLUpdater` can do that:
|
||||
|
||||
```objc
|
||||
[[self.updater relaunchToInstallUpdate] subscribeError:^(NSError *error) {
|
||||
NSLog(@"Error preparing update: %@", error);
|
||||
}];
|
||||
```
|
||||
|
||||
# Update JSON Format
|
||||
@@ -100,7 +110,6 @@ will check for an update again at the interval you specify.
|
||||
|
||||
# User Interface
|
||||
|
||||
Squirrel does not provide an updates interface, if you want to display available
|
||||
updates, subscribe to the `SQRLUpdaterUpdateAvailableNotification` notification.
|
||||
|
||||

|
||||
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-notifications).
|
||||
|
||||
@@ -7,20 +7,42 @@
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
5316AF7A188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */; };
|
||||
5316AF7B188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */; };
|
||||
5316AF95188EFFA100C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */; };
|
||||
5316AFA8188F002100C070DF /* SQRLDownload+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AFA6188F002100C070DF /* SQRLDownload+Private.h */; };
|
||||
5316AFAC188F0A8E00C070DF /* NSData+SQRLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */; };
|
||||
5316AFAD188F0A8E00C070DF /* NSData+SQRLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */; };
|
||||
533076EB17EB688300BDCCE0 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D097B9BC17BB4777006C3FEB /* IOKit.framework */; };
|
||||
53390936184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */; };
|
||||
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */; };
|
||||
5339094A184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */; };
|
||||
534FF35B17D8E8B90020A51A /* unused-helper in Copy LoginItems */ = {isa = PBXBuildFile; fileRef = D0B1DDCB17B487D90059C355 /* unused-helper */; };
|
||||
534FF36117D8E90A0020A51A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F60CA7EA179FC4F60069F69A /* Foundation.framework */; };
|
||||
534FF36717D8E90A0020A51A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534FF36517D8E90A0020A51A /* InfoPlist.strings */; };
|
||||
534FF36A17D8E90A0020A51A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 534FF36917D8E90A0020A51A /* main.m */; };
|
||||
534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; };
|
||||
53542FC0184F86EC003A6AFD /* Mantle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D0A56BA41804B1FD00A84EDC /* Mantle.framework */; };
|
||||
53542FCE184F86F1003A6AFD /* ReactiveCocoa.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D0F3F05617E83A1D003AFA6B /* ReactiveCocoa.framework */; };
|
||||
53542FCF184F870C003A6AFD /* Mantle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A56BA41804B1FD00A84EDC /* Mantle.framework */; };
|
||||
53542FD2184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */; };
|
||||
536CD4E2183E5D7F000C0FD1 /* SQRLDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = 536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */; };
|
||||
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */; };
|
||||
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; };
|
||||
53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; };
|
||||
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
|
||||
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A60406182134F9002DB2C7 /* Squirrel-Constants.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A60407182134F9002DB2C7 /* Squirrel-Constants.m */; };
|
||||
53A6040A182134F9002DB2C7 /* Squirrel-Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A60407182134F9002DB2C7 /* Squirrel-Constants.m */; };
|
||||
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 53AACF4917E9CA0500B41027 /* SQRLUpdate.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */; };
|
||||
53B0360B184E5C860079FB77 /* SQRLHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */; };
|
||||
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */; };
|
||||
53C3186F1816C38A0074B481 /* SQRLDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C318691816C38A0074B481 /* SQRLDownloadManager.h */; };
|
||||
53C318701816C38A0074B481 /* SQRLDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */; };
|
||||
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C3186B1816C38A0074B481 /* SQRLURLConnection.h */; };
|
||||
53C318721816C38A0074B481 /* SQRLURLConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186C1816C38A0074B481 /* SQRLURLConnection.m */; };
|
||||
53C318731816C38A0074B481 /* SQRLResumableDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */; };
|
||||
53C318741816C38A0074B481 /* SQRLResumableDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */; };
|
||||
53C3187C1816CCD70074B481 /* SQRLDownloadManagerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */; };
|
||||
53C3187D1816CCD70074B481 /* SQRLURLConnectionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */; };
|
||||
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = D000218D17BACEFF0050109A /* SQRLZipArchiver.m */; };
|
||||
D000219717BAD34D0050109A /* TestApplication.app.zip in Resources */ = {isa = PBXBuildFile; fileRef = D000219617BAD34D0050109A /* TestApplication.app.zip */; };
|
||||
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */; };
|
||||
@@ -109,6 +131,34 @@
|
||||
remoteGlobalIDString = 534FF35F17D8E90A0020A51A;
|
||||
remoteInfo = TestService;
|
||||
};
|
||||
53B70020181E944F000FFBD0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 09793579161B6251006DB5D5;
|
||||
remoteInfo = "OHHTTPStubs-iOS";
|
||||
};
|
||||
53B70022181E944F000FFBD0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 098368CE168FC7920082B1A4;
|
||||
remoteInfo = UnitTests;
|
||||
};
|
||||
53B70024181E944F000FFBD0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = F6C37BA016F6C8680082B630;
|
||||
remoteInfo = OHHTTPStubs;
|
||||
};
|
||||
53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = F6C37B9F16F6C8680082B630;
|
||||
remoteInfo = OHHTTPStubs;
|
||||
};
|
||||
D014AC1617B9793A007D79D0 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = D0C22BCE179CC00E00158214 /* Project object */;
|
||||
@@ -361,6 +411,8 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
53542FCE184F86F1003A6AFD /* ReactiveCocoa.framework in Copy Frameworks */,
|
||||
53542FC0184F86EC003A6AFD /* Mantle.framework in Copy Frameworks */,
|
||||
D08D4E4917B451F20012B22D /* Squirrel.framework in Copy Frameworks */,
|
||||
);
|
||||
name = "Copy Frameworks";
|
||||
@@ -380,11 +432,22 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RACSignal+SQRLFileManagerExtensions.h"; sourceTree = "<group>"; };
|
||||
5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RACSignal+SQRLFileManagerExtensions.m"; sourceTree = "<group>"; };
|
||||
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQRLDownload+Private.h"; sourceTree = "<group>"; };
|
||||
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+SQRLExtensions.h"; sourceTree = "<group>"; };
|
||||
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+SQRLExtensions.m"; sourceTree = "<group>"; };
|
||||
53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSHTTPURLResponse+SQRLExtensions.h"; sourceTree = "<group>"; };
|
||||
53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSHTTPURLResponse+SQRLExtensions.m"; sourceTree = "<group>"; };
|
||||
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSHTTPURLResponseExtensionsSpec.m; sourceTree = "<group>"; };
|
||||
534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = com.github.Squirrel.TestApplication.TestService.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
534FF36417D8E90A0020A51A /* TestService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TestService-Info.plist"; sourceTree = "<group>"; };
|
||||
534FF36617D8E90A0020A51A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
534FF36817D8E90A0020A51A /* TestService-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestService-Prefix.pch"; sourceTree = "<group>"; };
|
||||
534FF36917D8E90A0020A51A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
|
||||
53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadedUpdateSpec.m; sourceTree = "<group>"; };
|
||||
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLDownload.h; sourceTree = "<group>"; };
|
||||
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownload.m; sourceTree = "<group>"; };
|
||||
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDeepCodesignSpec.m; sourceTree = "<group>"; };
|
||||
53710D7D17D8F5C300A992DE /* bootstrap */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bootstrap; sourceTree = "<group>"; };
|
||||
53710D7E17D8F5C300A992DE /* cibuild */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = cibuild; sourceTree = "<group>"; };
|
||||
@@ -394,10 +457,19 @@
|
||||
53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = "<group>"; };
|
||||
53710D8317D8F5C300A992DE /* xcodebuild.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = xcodebuild.awk; sourceTree = "<group>"; };
|
||||
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
|
||||
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Squirrel-Constants.h"; sourceTree = "<group>"; };
|
||||
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Squirrel-Constants.m"; sourceTree = "<group>"; };
|
||||
53AACF4917E9CA0500B41027 /* SQRLUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLUpdate.h; sourceTree = "<group>"; };
|
||||
53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdate.m; sourceTree = "<group>"; };
|
||||
53B03609184E5C860079FB77 /* SQRLHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLHTTPServer.h; sourceTree = "<group>"; };
|
||||
53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLHTTPServer.m; sourceTree = "<group>"; };
|
||||
53C318691816C38A0074B481 /* SQRLDownloadManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLDownloadManager.h; sourceTree = "<group>"; };
|
||||
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadManager.m; sourceTree = "<group>"; };
|
||||
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLURLConnection.h; sourceTree = "<group>"; };
|
||||
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLURLConnection.m; sourceTree = "<group>"; };
|
||||
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLResumableDownload.h; sourceTree = "<group>"; };
|
||||
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLResumableDownload.m; sourceTree = "<group>"; };
|
||||
53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadManagerSpec.m; sourceTree = "<group>"; };
|
||||
53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLURLConnectionSpec.m; sourceTree = "<group>"; };
|
||||
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OHHTTPStubs.xcodeproj; path = External/ReactiveCocoa/external/specta/../../../OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj; sourceTree = "<group>"; };
|
||||
D000218C17BACEFF0050109A /* SQRLZipArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLZipArchiver.h; sourceTree = "<group>"; };
|
||||
D000218D17BACEFF0050109A /* SQRLZipArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLZipArchiver.m; sourceTree = "<group>"; };
|
||||
D000219617BAD34D0050109A /* TestApplication.app.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = TestApplication.app.zip; sourceTree = "<group>"; };
|
||||
@@ -521,6 +593,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
53542FCF184F870C003A6AFD /* Mantle.framework in Frameworks */,
|
||||
D0F3F06417E83B0B003AFA6B /* ReactiveCocoa.framework in Frameworks */,
|
||||
D08D4E4717B451E50012B22D /* Squirrel.framework in Frameworks */,
|
||||
D08D4E0D17B451500012B22D /* Cocoa.framework in Frameworks */,
|
||||
@@ -545,6 +618,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */,
|
||||
D0A56BCF1804B8FC00A84EDC /* Mantle.framework in Frameworks */,
|
||||
D093682417FF732200BA0EAE /* ServiceManagement.framework in Frameworks */,
|
||||
D0F3F06817E83B27003AFA6B /* ReactiveCocoa.framework in Frameworks */,
|
||||
@@ -596,13 +670,30 @@
|
||||
path = script;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
53A603F7182134D3002DB2C7 /* Supporting Files */ = {
|
||||
53B70015181E944F000FFBD0 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */,
|
||||
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */,
|
||||
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */,
|
||||
53B70023181E944F000FFBD0 /* UnitTests.octest */,
|
||||
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
53C3185A1816C3730074B481 /* Downloads */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
53C318691816C38A0074B481 /* SQRLDownloadManager.h */,
|
||||
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */,
|
||||
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */,
|
||||
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */,
|
||||
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */,
|
||||
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */,
|
||||
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */,
|
||||
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */,
|
||||
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */,
|
||||
);
|
||||
name = Downloads;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D00F5B8217E82CFB009A4818 /* Extensions */ = {
|
||||
@@ -610,6 +701,8 @@
|
||||
children = (
|
||||
D00F5B8917E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h */,
|
||||
D00F5B8A17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m */,
|
||||
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */,
|
||||
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */,
|
||||
);
|
||||
name = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -644,6 +737,10 @@
|
||||
D0964B3B17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m */,
|
||||
D0EDBE9A17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.h */,
|
||||
D0EDBE9B17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m */,
|
||||
53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */,
|
||||
53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */,
|
||||
5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */,
|
||||
5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */,
|
||||
);
|
||||
name = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -666,7 +763,8 @@
|
||||
534FF36217D8E90A0020A51A /* TestService */,
|
||||
D08D4E0F17B451500012B22D /* Supporting Files */,
|
||||
);
|
||||
path = TestApplication;
|
||||
name = TestApplication;
|
||||
path = ../TestApplication;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D08D4E0F17B451500012B22D /* Supporting Files */ = {
|
||||
@@ -753,7 +851,6 @@
|
||||
D0EDBE9F17B0C6930058BC3C /* Shared */,
|
||||
D0C22BE0179CC00E00158214 /* Squirrel */,
|
||||
D0C22BF6179CC00E00158214 /* SquirrelTests */,
|
||||
D08D4E0E17B451500012B22D /* TestApplication */,
|
||||
D014AC0317B97885007D79D0 /* ShipIt */,
|
||||
D0C22BD9179CC00E00158214 /* Frameworks */,
|
||||
53710D7C17D8F5C300A992DE /* Scripts */,
|
||||
@@ -780,6 +877,7 @@
|
||||
children = (
|
||||
D097B9BC17BB4777006C3FEB /* IOKit.framework */,
|
||||
D014ABB917B97403007D79D0 /* ServiceManagement.framework */,
|
||||
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */,
|
||||
D0F3F01417E83918003AFA6B /* Expecta.xcodeproj */,
|
||||
D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */,
|
||||
D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */,
|
||||
@@ -820,6 +918,7 @@
|
||||
D0C22BF6179CC00E00158214 /* SquirrelTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D08D4E0E17B451500012B22D /* TestApplication */,
|
||||
D0C22C4E179CC18000158214 /* Specs */,
|
||||
D0C22BF7179CC00E00158214 /* Supporting Files */,
|
||||
);
|
||||
@@ -831,6 +930,8 @@
|
||||
children = (
|
||||
D049059E180558B7004E683E /* SQRLTestUpdate.h */,
|
||||
D049059F180558B7004E683E /* SQRLTestUpdate.m */,
|
||||
53B03609184E5C860079FB77 /* SQRLHTTPServer.h */,
|
||||
53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */,
|
||||
D000219617BAD34D0050109A /* TestApplication.app.zip */,
|
||||
D09D244917B59AB30001FAF8 /* TestApplication 2.1.app */,
|
||||
D0C22BF8179CC00E00158214 /* SquirrelTests-Info.plist */,
|
||||
@@ -908,6 +1009,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D0964B2817F2DEE700D88BF7 /* Updates */,
|
||||
53C3185A1816C3730074B481 /* Downloads */,
|
||||
D0964B3417F2DEF500D88BF7 /* Archives */,
|
||||
D0964B3517F2DF3100D88BF7 /* IPC */,
|
||||
);
|
||||
@@ -918,6 +1020,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D00F5B8D17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m */,
|
||||
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */,
|
||||
D0EDBEE017B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m */,
|
||||
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */,
|
||||
D049059A180554BA004E683E /* SQRLDirectoryManagerSpec.m */,
|
||||
@@ -929,6 +1032,9 @@
|
||||
D0C22C4F179CC18C00158214 /* SQRLUpdaterSpec.m */,
|
||||
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */,
|
||||
D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */,
|
||||
53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */,
|
||||
53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */,
|
||||
53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */,
|
||||
);
|
||||
name = Specs;
|
||||
sourceTree = "<group>";
|
||||
@@ -940,7 +1046,6 @@
|
||||
D0A56BF61804BABB00A84EDC /* Code Signing */,
|
||||
D0A56BE71804BA2400A84EDC /* State */,
|
||||
D06B58BC18032BE800656D97 /* Transactions */,
|
||||
53A603F7182134D3002DB2C7 /* Supporting Files */,
|
||||
);
|
||||
name = Shared;
|
||||
path = Squirrel;
|
||||
@@ -989,12 +1094,19 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */,
|
||||
D0964B3C17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.h in Headers */,
|
||||
D0C22C4C179CC15100158214 /* SQRLUpdater.h in Headers */,
|
||||
5316AFAC188F0A8E00C070DF /* NSData+SQRLExtensions.h in Headers */,
|
||||
D0C22C52179CC23900158214 /* Squirrel.h in Headers */,
|
||||
5316AFA8188F002100C070DF /* SQRLDownload+Private.h in Headers */,
|
||||
53390936184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h in Headers */,
|
||||
D0964B3817F2E01500D88BF7 /* SQRLDownloadedUpdate.h in Headers */,
|
||||
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */,
|
||||
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */,
|
||||
5316AF7A188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h in Headers */,
|
||||
53C318731816C38A0074B481 /* SQRLResumableDownload.h in Headers */,
|
||||
536CD4E2183E5D7F000C0FD1 /* SQRLDownload.h in Headers */,
|
||||
53C3186F1816C38A0074B481 /* SQRLDownloadManager.h in Headers */,
|
||||
D00F5B8B17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1089,11 +1201,11 @@
|
||||
D0C22BEA179CC00E00158214 /* Sources */,
|
||||
D0C22BEB179CC00E00158214 /* Frameworks */,
|
||||
D0C22BEC179CC00E00158214 /* Resources */,
|
||||
D0C22BED179CC00E00158214 /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
53B7002F181E9456000FFBD0 /* PBXTargetDependency */,
|
||||
D0F3F03317E83924003AFA6B /* PBXTargetDependency */,
|
||||
D0F3F03517E83924003AFA6B /* PBXTargetDependency */,
|
||||
D08D4E4C17B4520C0012B22D /* PBXTargetDependency */,
|
||||
@@ -1134,6 +1246,10 @@
|
||||
ProductGroup = D0A56B971804B1FD00A84EDC /* Products */;
|
||||
ProjectRef = D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 53B70015181E944F000FFBD0 /* Products */;
|
||||
ProjectRef = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = D0F3F04B17E83A1D003AFA6B /* Products */;
|
||||
ProjectRef = D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */;
|
||||
@@ -1155,6 +1271,27 @@
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXReferenceProxy section */
|
||||
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
path = "libOHHTTPStubs-iOS.a";
|
||||
remoteRef = 53B70020181E944F000FFBD0 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
53B70023181E944F000FFBD0 /* UnitTests.octest */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.cfbundle;
|
||||
path = UnitTests.octest;
|
||||
remoteRef = 53B70022181E944F000FFBD0 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.framework;
|
||||
path = OHHTTPStubs.framework;
|
||||
remoteRef = 53B70024181E944F000FFBD0 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
D0964B1C17F2DD4500D88BF7 /* libReactiveCocoa-Mac.a */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = archive.ar;
|
||||
@@ -1340,22 +1477,6 @@
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
D0C22BED179CC00E00158214 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
534FF35C17D8E90A0020A51A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
@@ -1373,9 +1494,9 @@
|
||||
D014AC1117B9789C007D79D0 /* SQRLInstaller.m in Sources */,
|
||||
D043F77217FF40C90012F996 /* SQRLTerminationListener.m in Sources */,
|
||||
D014AC0517B97885007D79D0 /* ShipIt-main.m in Sources */,
|
||||
5316AF95188EFFA100C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */,
|
||||
D014AC1917B979A8007D79D0 /* SQRLCodeSignature.m in Sources */,
|
||||
D06B58B618032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
|
||||
53A6040A182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
|
||||
D014AC1A17B979AA007D79D0 /* NSError+SQRLVerbosityExtensions.m in Sources */,
|
||||
D0D2B6271804E903000EA901 /* SQRLDirectoryManager.m in Sources */,
|
||||
D0964B3E17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m in Sources */,
|
||||
@@ -1396,19 +1517,25 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5316AFAD188F0A8E00C070DF /* NSData+SQRLExtensions.m in Sources */,
|
||||
D0964B3D17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m in Sources */,
|
||||
F6F2ED8417A33C4900F4D220 /* SQRLCodeSignature.m in Sources */,
|
||||
D0D2B6261804E903000EA901 /* SQRLDirectoryManager.m in Sources */,
|
||||
53C318741816C38A0074B481 /* SQRLResumableDownload.m in Sources */,
|
||||
5316AF7B188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */,
|
||||
D0964B3917F2E01500D88BF7 /* SQRLDownloadedUpdate.m in Sources */,
|
||||
D014ABB817B96CB3007D79D0 /* SQRLShipItLauncher.m in Sources */,
|
||||
53C318721816C38A0074B481 /* SQRLURLConnection.m in Sources */,
|
||||
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */,
|
||||
D0C22C4D179CC15100158214 /* SQRLUpdater.m in Sources */,
|
||||
D0EDBE9D17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m in Sources */,
|
||||
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
|
||||
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
|
||||
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
|
||||
53C318701816C38A0074B481 /* SQRLDownloadManager.m in Sources */,
|
||||
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
|
||||
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */,
|
||||
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
|
||||
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1418,15 +1545,20 @@
|
||||
files = (
|
||||
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */,
|
||||
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */,
|
||||
53542FD2184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m in Sources */,
|
||||
D043F77E17FF43640012F996 /* SQRLTerminationListener.m in Sources */,
|
||||
D09D244117B595470001FAF8 /* SQRLInstallerSpec.m in Sources */,
|
||||
D0C22C50179CC18C00158214 /* SQRLUpdaterSpec.m in Sources */,
|
||||
D0EDBEDD17B0DB3E0058BC3C /* SQRLTestCase.m in Sources */,
|
||||
5339094A184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m in Sources */,
|
||||
53C3187C1816CCD70074B481 /* SQRLDownloadManagerSpec.m in Sources */,
|
||||
D06B58D118035B5A00656D97 /* SQRLTerminationListenerSpec.m in Sources */,
|
||||
D0EDBEE117B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m in Sources */,
|
||||
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */,
|
||||
D00F5B8E17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m in Sources */,
|
||||
53B0360B184E5C860079FB77 /* SQRLHTTPServer.m in Sources */,
|
||||
D049059B180554BA004E683E /* SQRLDirectoryManagerSpec.m in Sources */,
|
||||
53C3187D1816CCD70074B481 /* SQRLURLConnectionSpec.m in Sources */,
|
||||
D061E40217FD580D00CC134A /* SQRLInstaller.m in Sources */,
|
||||
D04905A0180558B7004E683E /* SQRLTestUpdate.m in Sources */,
|
||||
D049059D18055671004E683E /* SQRLShipItStateSpec.m in Sources */,
|
||||
@@ -1441,6 +1573,11 @@
|
||||
target = 534FF35F17D8E90A0020A51A /* TestService */;
|
||||
targetProxy = 534FF37017D8E9170020A51A /* PBXContainerItemProxy */;
|
||||
};
|
||||
53B7002F181E9456000FFBD0 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = OHHTTPStubs;
|
||||
targetProxy = 53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D014AC1717B9793A007D79D0 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D014AC0017B97885007D79D0 /* ShipIt */;
|
||||
|
||||
@@ -20,6 +20,20 @@
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -97,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, ... {
|
||||
@@ -117,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
|
||||
|
||||
@@ -9,6 +9,16 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <Mantle/Mantle.h>
|
||||
|
||||
// The domain for errors originating within SQRLCodeSignature.
|
||||
extern NSString * const SQRLCodeSignatureErrorDomain;
|
||||
|
||||
// The bundle did not pass codesign verification.
|
||||
extern const NSInteger SQRLCodeSignatureErrorDidNotPass;
|
||||
|
||||
// A static code object could not be created for the target bundle or running
|
||||
// code.
|
||||
extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// Implements the verification of Apple code signatures.
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <Security/Security.h>
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
|
||||
|
||||
const NSInteger SQRLCodeSignatureErrorDidNotPass = -1;
|
||||
const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
|
||||
|
||||
@interface SQRLCodeSignature ()
|
||||
|
||||
@@ -79,7 +83,7 @@
|
||||
- (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);
|
||||
@@ -95,9 +99,11 @@
|
||||
NSString *failureReason = CFBridgingRelease(SecCopyErrorMessageString(result, NULL));
|
||||
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
|
||||
|
||||
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
|
||||
return nil;
|
||||
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscriber.disposable.disposed) return;
|
||||
|
||||
CFErrorRef validityError = NULL;
|
||||
result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, (__bridge SecRequirementRef)self.requirement, &validityError);
|
||||
@@ -114,12 +120,11 @@
|
||||
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
|
||||
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
|
||||
|
||||
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
|
||||
return nil;
|
||||
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
|
||||
return;
|
||||
}
|
||||
|
||||
[subscriber sendCompleted];
|
||||
return nil;
|
||||
}] setNameWithFormat:@"-verifyCodeSignatureOfBundle: %@", bundleURL];
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,27 @@
|
||||
// on-disk locations for. This must not be nil.
|
||||
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
|
||||
|
||||
// Finds or creates an Application Support 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 *)applicationSupportURL;
|
||||
@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;
|
||||
@property (readonly, nonatomic) RACSignal *shipItStateURL;
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import "RACSignal+SQRLFileManagerExtensions.h"
|
||||
|
||||
@interface SQRLDirectoryManager ()
|
||||
|
||||
@@ -53,25 +55,65 @@
|
||||
|
||||
#pragma mark Folder URLs
|
||||
|
||||
+ (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
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
NSError *error = nil;
|
||||
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory 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];
|
||||
}]
|
||||
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
|
||||
applicationSupportURL]
|
||||
|
||||
@@ -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 [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
|
||||
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
|
||||
}];
|
||||
return @{};
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)updateJSONTransformer {
|
||||
return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:SQRLUpdate.class];
|
||||
}
|
||||
|
||||
+ (NSValueTransformer *)bundleURLJSONTransformer {
|
||||
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
+14
-10
@@ -8,6 +8,9 @@
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// The domain for errors originating within SQRLInstaller.
|
||||
extern NSString * const SQRLInstallerErrorDomain;
|
||||
|
||||
// There was an error copying the target bundle to the backup location.
|
||||
extern const NSInteger SQRLInstallerErrorBackupFailed;
|
||||
|
||||
@@ -31,7 +34,9 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
|
||||
// There was an error moving a bundle across volumes.
|
||||
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
|
||||
|
||||
@class RACCommand;
|
||||
@class RACSignal;
|
||||
@class RACAction;
|
||||
@class SQRLShipItState;
|
||||
@class SQRLDirectoryManager;
|
||||
|
||||
// Performs the installation of an update, using the `SQRLShipItState` on disk,
|
||||
@@ -41,21 +46,20 @@ extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
|
||||
// terminated.
|
||||
@interface SQRLInstaller : NSObject
|
||||
|
||||
// When executed with a `SQRLShipItState`, attempts to install the update or
|
||||
// resume an in-progress installation.
|
||||
// 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 `SQRLShipItState`, 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.
|
||||
|
||||
+50
-90
@@ -18,7 +18,8 @@
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import <sys/xattr.h>
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
|
||||
|
||||
const NSInteger SQRLInstallerErrorBackupFailed = -1;
|
||||
const NSInteger SQRLInstallerErrorReplacingTarget = -2;
|
||||
@@ -144,44 +145,34 @@ typedef struct {
|
||||
if (self == nil) return nil;
|
||||
|
||||
_directoryManager = directoryManager;
|
||||
|
||||
@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:^(SQRLShipItState *state) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
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];
|
||||
}];
|
||||
|
||||
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItState *state) {
|
||||
@strongify(self);
|
||||
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];
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark Actions
|
||||
|
||||
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state {
|
||||
NSParameterAssert(state != nil);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
- (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 Installer State
|
||||
|
||||
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
|
||||
@@ -210,7 +201,6 @@ typedef struct {
|
||||
{ .installerState = SQRLInstallerStateBackingUp, .selector = @selector(backUpWithState:) },
|
||||
{ .installerState = SQRLInstallerStateInstalling, .selector = @selector(installWithState:) },
|
||||
{ .installerState = SQRLInstallerStateVerifyingInPlace, .selector = @selector(verifyInPlaceWithState:) },
|
||||
{ .installerState = SQRLInstallerStateRelaunching, .selector = @selector(relaunchWithState:) },
|
||||
};
|
||||
|
||||
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
|
||||
@@ -235,7 +225,7 @@ typedef struct {
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
|
||||
}
|
||||
|
||||
SEL selector = dispatchTable[tableIndex].selector;
|
||||
@@ -262,13 +252,12 @@ typedef struct {
|
||||
nextState = dispatchTable[tableIndex + 1].installerState;
|
||||
}
|
||||
|
||||
return [[step
|
||||
return [[[step
|
||||
doCompleted:^{
|
||||
NSLog(@"Completed state %i", (int)installerState);
|
||||
}]
|
||||
then:^{
|
||||
return [RACSignal return:@(nextState)];
|
||||
}];
|
||||
ignoreValues]
|
||||
concat:[RACSignal return:@(nextState)]];
|
||||
}];
|
||||
|
||||
return [[self
|
||||
@@ -365,11 +354,9 @@ typedef struct {
|
||||
// failure. Try recovering it if it did.
|
||||
return [[self
|
||||
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
|
||||
then:^{
|
||||
// Recovery succeeded, but we still want to pass
|
||||
// through the original error.
|
||||
return [RACSignal error:error];
|
||||
}];
|
||||
// Recovery succeeded, but we still want to pass
|
||||
// through the original error.
|
||||
concat:[RACSignal error:error]];
|
||||
}];
|
||||
}]
|
||||
flatten]
|
||||
@@ -387,37 +374,14 @@ typedef struct {
|
||||
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
|
||||
return [[self
|
||||
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
|
||||
then:^{
|
||||
return [[self
|
||||
deleteBackupAtURL:backupBundleURL]
|
||||
catchTo:[RACSignal empty]];
|
||||
}];
|
||||
concat:[[self
|
||||
deleteBackupAtURL:backupBundleURL]
|
||||
catchTo:[RACSignal empty]]];
|
||||
}]
|
||||
flatten]
|
||||
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
|
||||
}
|
||||
|
||||
- (RACSignal *)relaunchWithState:(SQRLShipItState *)state {
|
||||
return [[[[RACSignal
|
||||
defer:^{
|
||||
if (state.relaunchAfterInstallation) {
|
||||
return [self getRequiredKey:@keypath(state.targetBundleURL) fromState:state];
|
||||
} else {
|
||||
return [RACSignal empty];
|
||||
}
|
||||
}]
|
||||
deliverOn:RACScheduler.mainThreadScheduler]
|
||||
flattenMap:^(NSURL *bundleURL) {
|
||||
NSError *error = nil;
|
||||
if ([NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}]
|
||||
setNameWithFormat:@"%@ -relaunch", self];
|
||||
}
|
||||
|
||||
#pragma mark Backing Up
|
||||
|
||||
- (RACSignal *)backUpBundleAtURL:(NSURL *)targetBundleURL {
|
||||
@@ -459,26 +423,23 @@ typedef struct {
|
||||
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
|
||||
NSParameterAssert(backupURL != nil);
|
||||
|
||||
return [[[RACSignal
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
NSError *error = nil;
|
||||
if ([NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
|
||||
return [RACSignal empty];
|
||||
} else {
|
||||
if (![NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}]
|
||||
then:^{
|
||||
|
||||
// Also remove the temporary directory that the backup lived in.
|
||||
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 {
|
||||
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) != 0) {
|
||||
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
|
||||
}
|
||||
@@ -492,10 +453,11 @@ typedef struct {
|
||||
catch:^(NSError *error) {
|
||||
if (backupBundleURL == nil) return [RACSignal error:error];
|
||||
|
||||
return [[[[self
|
||||
installItemAtURL:bundleURL fromURL:backupBundleURL]
|
||||
initially:^{
|
||||
return [[[RACSignal
|
||||
defer:^{
|
||||
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
|
||||
|
||||
return [self installItemAtURL:bundleURL fromURL:backupBundleURL];
|
||||
}]
|
||||
doCompleted:^{
|
||||
NSLog(@"Restored backup bundle to %@", bundleURL);
|
||||
@@ -516,9 +478,7 @@ typedef struct {
|
||||
|
||||
return [[[[self
|
||||
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
|
||||
then:^{
|
||||
return [RACSignal return:@YES];
|
||||
}]
|
||||
concat:[RACSignal return:@YES]]
|
||||
catch:^(NSError *error) {
|
||||
BOOL directory;
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
|
||||
@@ -584,7 +544,7 @@ typedef struct {
|
||||
return YES;
|
||||
}];
|
||||
|
||||
return enumerator.rac_sequence.signal;
|
||||
return [enumerator.rac_promise start];
|
||||
}]
|
||||
flattenMap:^(NSURL *URL) {
|
||||
const char *path = URL.path.fileSystemRepresentation;
|
||||
@@ -618,7 +578,7 @@ typedef struct {
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
|
||||
};
|
||||
|
||||
return [NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
|
||||
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
|
||||
}
|
||||
|
||||
- (NSError *)errorByAddingDescription:(NSString *)description code:(NSInteger)code toError:(NSError *)error {
|
||||
@@ -627,7 +587,7 @@ typedef struct {
|
||||
if (description != nil) userInfo[NSLocalizedDescriptionKey] = description;
|
||||
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
|
||||
|
||||
return [NSError errorWithDomain:SQRLErrorDomain code:code userInfo:userInfo];
|
||||
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:code userInfo:userInfo];
|
||||
}
|
||||
|
||||
@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
|
||||
@@ -10,6 +10,12 @@
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
// The domain for errors originating within SQRLShipItLauncher.
|
||||
extern NSString * const SQRLShipItLauncherErrorDomain;
|
||||
|
||||
// The ShipIt service could not be started.
|
||||
extern const NSInteger SQRLShipItLauncherErrorCouldNotStartService;
|
||||
|
||||
// Responsible for launching the ShipIt service to actually install an update.
|
||||
@interface SQRLShipItLauncher : NSObject
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@
|
||||
#import <Security/Security.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <launch.h>
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
|
||||
|
||||
const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
|
||||
@implementation SQRLShipItLauncher
|
||||
|
||||
@@ -68,7 +71,7 @@
|
||||
|
||||
+ (RACSignal *)shipItAuthorization {
|
||||
return [[RACSignal
|
||||
createSignal:^(id<RACSubscriber> subscriber) {
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
AuthorizationItem rightItems[] = {
|
||||
{
|
||||
.name = kSMRightModifySystemDaemons,
|
||||
@@ -112,9 +115,9 @@
|
||||
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
|
||||
}
|
||||
|
||||
return [RACDisposable disposableWithBlock:^{
|
||||
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
|
||||
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
|
||||
}];
|
||||
}]];
|
||||
}]
|
||||
setNameWithFormat:@"+shipItAuthorization"];
|
||||
}
|
||||
@@ -129,13 +132,11 @@
|
||||
|
||||
CFErrorRef cfError;
|
||||
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
|
||||
#if DEBUG
|
||||
NSLog(@"Could not remove previous ShipIt job: %@", cfError);
|
||||
#endif
|
||||
NSError *error = CFBridgingRelease(cfError);
|
||||
cfError = NULL;
|
||||
|
||||
if (cfError != NULL) {
|
||||
CFRelease(cfError);
|
||||
cfError = NULL;
|
||||
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
|
||||
NSLog(@"Could not remove previous ShipIt job: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,22 @@
|
||||
|
||||
#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;
|
||||
@@ -25,9 +41,6 @@ extern NSString * const SQRLShipItStatePropertyErrorKey;
|
||||
// update bundle.
|
||||
// SQRLInstallerStateVerifyingInPlace - Verifying that the target bundle is
|
||||
// still valid after updating.
|
||||
// SQRLInstallerStateRelaunching - Relaunching the updated application.
|
||||
// This state will be entered even if
|
||||
// there's no relaunching to do.
|
||||
//
|
||||
// Note that these values must remain backwards compatible, so ShipIt doesn't
|
||||
// start up in a weird mode on a newer version.
|
||||
@@ -37,7 +50,6 @@ typedef enum : NSInteger {
|
||||
SQRLInstallerStateBackingUp,
|
||||
SQRLInstallerStateInstalling,
|
||||
SQRLInstallerStateVerifyingInPlace,
|
||||
SQRLInstallerStateRelaunching
|
||||
} SQRLInstallerState;
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
@@ -8,10 +8,14 @@
|
||||
|
||||
#import "SQRLShipItState.h"
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "Squirrel-Constants.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
|
||||
@@ -29,7 +33,7 @@ NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErro
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
|
||||
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
@@ -74,7 +78,7 @@ NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErro
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:state];
|
||||
@@ -93,7 +97,7 @@ NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErro
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:data];
|
||||
|
||||
@@ -45,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];
|
||||
@@ -60,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, ^{
|
||||
@@ -72,10 +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
|
||||
+26
-10
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
@@ -134,16 +136,30 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
|
||||
|
||||
- (BOOL)validateUpdateURL:(NSURL **)updateURLPtr error:(NSError **)error {
|
||||
NSURL *updateURL = *updateURLPtr;
|
||||
if (![updateURL isKindOfClass:NSURL.class] || updateURL.scheme == nil || updateURL.host == nil || updateURL.path == nil) {
|
||||
if (![updateURL isKindOfClass:NSURL.class]) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An invalid updateURL was given to SQRLUpdate: %@", nil), updateURL]
|
||||
};
|
||||
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
|
||||
}
|
||||
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL valid = (updateURL.scheme != nil);
|
||||
valid &= ([updateURL.scheme isEqualToString:@"file"] || updateURL.host != nil);
|
||||
valid &= (updateURL.path != nil);
|
||||
if (!valid) {
|
||||
if (error != NULL) {
|
||||
NSDictionary *userInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Update URLs must have a scheme, a host and a path: %@", nil), updateURL]
|
||||
};
|
||||
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
+49
-12
@@ -6,6 +6,35 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
// The domain for errors originating within SQRLUpdater.
|
||||
extern NSString * const SQRLUpdaterErrorDomain;
|
||||
|
||||
// The downloaded update does not contain an app bundle, or it was deleted on
|
||||
// disk before we could get to it.
|
||||
extern const NSInteger SQRLUpdaterErrorMissingUpdateBundle;
|
||||
|
||||
// An error occurred in the out-of-process updater while it was setting up.
|
||||
extern const NSInteger SQRLUpdaterErrorPreparingUpdateJob;
|
||||
|
||||
// The code signing requirement for the running application could not be
|
||||
// retrieved.
|
||||
extern const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement;
|
||||
|
||||
// The server sent a response that we didn't understand.
|
||||
//
|
||||
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
|
||||
extern const NSInteger SQRLUpdaterErrorInvalidServerResponse;
|
||||
|
||||
// The server sent a response body that we didn't understand.
|
||||
//
|
||||
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
|
||||
extern const NSInteger SQRLUpdaterErrorInvalidServerBody;
|
||||
|
||||
// The server sent update JSON that we didn't understand.
|
||||
//
|
||||
// Includes `SQRLUpdaterJSONObjectErrorKey` in the error's `userInfo`.
|
||||
extern const NSInteger SQRLUpdaterErrorInvalidJSON;
|
||||
|
||||
// Associated with the `NSData` received from the server when an error with code
|
||||
// `SQRLUpdaterErrorInvalidServerResponse` is generated.
|
||||
extern NSString * const SQRLUpdaterServerDataErrorKey;
|
||||
@@ -14,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;
|
||||
|
||||
@@ -23,21 +52,14 @@ 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;
|
||||
// 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;
|
||||
|
||||
// Whether or not to relaunch after installing an update.
|
||||
//
|
||||
// This will be reset to NO whenever update installation fails.
|
||||
@property (atomic) BOOL shouldRelaunch;
|
||||
|
||||
// The request that will be sent to check for updates.
|
||||
//
|
||||
// The default value is the argument that was originally passed to
|
||||
@@ -75,6 +97,21 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
|
||||
// checking.
|
||||
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval;
|
||||
|
||||
// Terminates the running application to install any available update, then
|
||||
// automatically relaunches the app after updating.
|
||||
//
|
||||
// This method is only useful if you want the application to automatically
|
||||
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
|
||||
// other exit mechanism.
|
||||
//
|
||||
// After invoking this method, the receiver is responsible for terminating the
|
||||
// application upon success. The app must not be terminated in any other way
|
||||
// unless an error occurs.
|
||||
//
|
||||
// Returns a signal that will error on the main scheduler if anything goes
|
||||
// wrong before termination. The signal will never complete.
|
||||
- (RACSignal *)relaunchToInstallUpdate;
|
||||
|
||||
@end
|
||||
|
||||
@interface SQRLUpdater (Unavailable)
|
||||
|
||||
+255
-156
@@ -17,16 +17,30 @@
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLShipItState.h"
|
||||
#import "SQRLUpdate.h"
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "Squirrel-Constants.h"
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLDownloadManager.h"
|
||||
|
||||
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
|
||||
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
|
||||
NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey";
|
||||
|
||||
const NSInteger SQRLUpdaterErrorMissingUpdateBundle = 2;
|
||||
const NSInteger SQRLUpdaterErrorPreparingUpdateJob = 3;
|
||||
const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement = 4;
|
||||
const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
|
||||
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
|
||||
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
|
||||
|
||||
@interface SQRLUpdater ()
|
||||
|
||||
// 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.
|
||||
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
|
||||
@@ -55,22 +69,25 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
// 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.
|
||||
@@ -103,16 +120,27 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
// 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
|
||||
@@ -131,39 +159,29 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
_updateRequest = [updateRequest copy];
|
||||
_updateClass = SQRLUpdate.class;
|
||||
|
||||
_directoryManager = SQRLDirectoryManager.currentApplicationManager;
|
||||
|
||||
NSError *error = nil;
|
||||
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
|
||||
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);
|
||||
_checkForUpdatesAction = [[[RACSignal
|
||||
defer:^{
|
||||
@strongify(self);
|
||||
return [RACSignal return:self.updateRequest];
|
||||
}]
|
||||
flattenMap:^(NSURLRequest *request) {
|
||||
@strongify(self);
|
||||
return [self checkForUpdates:request];
|
||||
}]
|
||||
action];
|
||||
|
||||
// TODO: Maybe allow this to be an argument to the command?
|
||||
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
_updates = [[RACSubject
|
||||
subject]
|
||||
setNameWithFormat:@"%@ updates", self];
|
||||
|
||||
return [[[[[[NSURLConnection
|
||||
rac_sendAsynchronousRequest:request]
|
||||
reduceEach:^(id _, NSData *data) {
|
||||
return data;
|
||||
}]
|
||||
flattenMap:^(NSData *data) {
|
||||
return [self updateFromJSONData:data];
|
||||
}]
|
||||
flattenMap:^(SQRLUpdate *update) {
|
||||
return [self downloadAndPrepareUpdate:update];
|
||||
}]
|
||||
doError:^(id _) {
|
||||
self.shouldRelaunch = NO;
|
||||
}]
|
||||
deliverOn:RACScheduler.mainThreadScheduler];
|
||||
}];
|
||||
|
||||
_shipItLauncher = [[[RACSignal
|
||||
_shipItLauncher = [[[[RACSignal
|
||||
defer:^{
|
||||
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
|
||||
|
||||
@@ -175,31 +193,80 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
// wait for another, more canonical error.
|
||||
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
|
||||
}]
|
||||
replayLazily]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
deferred]
|
||||
setNameWithFormat:@"shipItLauncher"];
|
||||
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[_updates sendCompleted];
|
||||
}
|
||||
|
||||
#pragma mark Checking for Updates
|
||||
|
||||
- (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 {
|
||||
@@ -216,7 +283,7 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
userInfo[SQRLUpdaterServerDataErrorKey] = data;
|
||||
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:userInfo]];
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
|
||||
}
|
||||
|
||||
Class updateClass = self.updateClass;
|
||||
@@ -233,7 +300,7 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
userInfo[SQRLUpdaterJSONObjectErrorKey] = JSON;
|
||||
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal return:update];
|
||||
@@ -241,14 +308,21 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
setNameWithFormat:@"%@ -updateFromJSONData:", self];
|
||||
}
|
||||
|
||||
#pragma Download
|
||||
|
||||
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update {
|
||||
NSParameterAssert(update != nil);
|
||||
|
||||
return [[[self
|
||||
uniqueTemporaryDirectoryForUpdate]
|
||||
return [[[self.directoryManager
|
||||
createUniqueUpdateDirectoryURL]
|
||||
flattenMap:^(NSURL *downloadDirectory) {
|
||||
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) {
|
||||
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
|
||||
}]
|
||||
@@ -262,74 +336,65 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
|
||||
}
|
||||
|
||||
- (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 = [NSMutableURLRequest requestWithURL:zipDownloadURL];
|
||||
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
return [[[NSURLConnection
|
||||
rac_sendAsynchronousRequest:zipDownloadRequest]
|
||||
reduceEach:^(id _, NSData *data) {
|
||||
return data;
|
||||
}]
|
||||
flattenMap:^(NSData *data) {
|
||||
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
|
||||
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:zipDownloadRequest];
|
||||
|
||||
NSError *error = nil;
|
||||
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
|
||||
return [RACSignal return:zipOutputURL];
|
||||
} else {
|
||||
return [RACSignal error:error];
|
||||
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 >= 200 && httpResponse.statusCode <= 299)) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
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]
|
||||
concat:[[downloadManager
|
||||
removeAllResumableDownloads]
|
||||
catchTo:RACSignal.empty]];
|
||||
}]
|
||||
doNext:^(NSURL *zipOutputURL) {
|
||||
NSLog(@"Download completed to: %@", zipOutputURL);
|
||||
}]
|
||||
flattenMap:^(NSURL *zipOutputURL) {
|
||||
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
|
||||
}]
|
||||
then:^{
|
||||
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
|
||||
}]
|
||||
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
|
||||
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 applicationSupportURL];
|
||||
return [SQRLZipArchiver unzipArchiveAtURL:archiveURL intoDirectoryAtURL:directoryURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *appSupportURL) {
|
||||
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.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 {
|
||||
@@ -342,25 +407,28 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
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];
|
||||
@@ -369,7 +437,7 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not locate update bundle for %@ within %@", nil), NSRunningApplication.currentApplication.bundleIdentifier, directory],
|
||||
};
|
||||
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
|
||||
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
|
||||
}
|
||||
}]
|
||||
map:^(NSURL *URL) {
|
||||
@@ -378,6 +446,15 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
setNameWithFormat:@"%@ -applicationBundleMatchingCurrentApplicationInDirectory: %@", self, directory];
|
||||
}
|
||||
|
||||
- (RACSignal *)shipItStateURL {
|
||||
return [[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return directoryManager.shipItStateURL;
|
||||
}]
|
||||
setNameWithFormat:@"%@ -shipItStateURL", self];
|
||||
}
|
||||
|
||||
#pragma mark Installing Updates
|
||||
|
||||
- (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle *)updateBundle {
|
||||
@@ -386,53 +463,75 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
|
||||
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:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
RACSignal *stateLocation = directoryManager.shipItStateURL;
|
||||
return [[[[SQRLShipItState
|
||||
readUsingURL:stateLocation]
|
||||
catchTo:[RACSignal empty]]
|
||||
flattenMap:^(SQRLShipItState *existingState) {
|
||||
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:SQRLErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
|
||||
}
|
||||
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
then:^{
|
||||
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
|
||||
state.relaunchAfterInstallation = self.shouldRelaunch;
|
||||
return [state writeUsingURL:stateLocation];
|
||||
}];
|
||||
}]
|
||||
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 [[[[[[[[[SQRLShipItState
|
||||
readUsingURL:self.shipItStateURL]
|
||||
flattenMap:^(SQRLShipItState *existingState) {
|
||||
return [self validateExistingState:existingState];
|
||||
}]
|
||||
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];
|
||||
}]
|
||||
deliverOn:RACScheduler.mainThreadScheduler]
|
||||
doCompleted:^{
|
||||
[NSApp terminate:self];
|
||||
}]
|
||||
// Never allow `completed` to escape this signal chain (in case
|
||||
// -terminate: is asynchronous or something crazy).
|
||||
concat:[RACSignal never]]
|
||||
promiseOnScheduler:RACScheduler.immediateScheduler]
|
||||
deferred]
|
||||
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,10 +10,17 @@
|
||||
|
||||
@class RACSignal;
|
||||
|
||||
extern NSString * const SQRLZipArchiverErrorDomain;
|
||||
|
||||
// Associated with an NSNumber containing the code that a shell task exited
|
||||
// with.
|
||||
extern NSString * const SQRLZipArchiverExitCodeErrorKey;
|
||||
|
||||
// `SQRLZipArchiver` tried to invoke the shell and failed.
|
||||
//
|
||||
// Contains `SQRLZipArchiverExitStatusErrorKey` in the `userInfo` dictionary.
|
||||
extern const NSInteger SQRLZipArchiverShellTaskFailed;
|
||||
|
||||
// Uses `ditto` on the command line to zip and unzip archives.
|
||||
@interface SQRLZipArchiver : NSObject
|
||||
|
||||
|
||||
+14
-15
@@ -9,9 +9,10 @@
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
NSString * const SQRLZipArchiverErrorDomain = @"SQRLZipArchiverErrorDomain";
|
||||
NSString * const SQRLZipArchiverExitCodeErrorKey = @"SQRLZipArchiverExitCodeErrorKey";
|
||||
const NSInteger SQRLZipArchiverShellTaskFailed = 1;
|
||||
|
||||
@interface SQRLZipArchiver () {
|
||||
RACSubject *_taskTerminated;
|
||||
@@ -129,25 +130,23 @@ NSString * const SQRLZipArchiverExitCodeErrorKey = @"SQRLZipArchiverExitCodeErro
|
||||
// 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:SQRLErrorDomain 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;
|
||||
|
||||
+43
-15
@@ -24,6 +24,9 @@
|
||||
// updating will abort.
|
||||
static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
|
||||
|
||||
// The domain for errors generated here.
|
||||
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
|
||||
|
||||
// Waits for all instances of the target application (as described in the
|
||||
// `state`) to exit, then sends completed.
|
||||
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
|
||||
@@ -52,16 +55,15 @@ int main(int argc, const char * argv[]) {
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
|
||||
RACSignal *stateLocation = directoryManager.shipItStateURL;
|
||||
|
||||
[[[[[[SQRLShipItState
|
||||
[[[[[[[SQRLShipItState
|
||||
readUsingURL:stateLocation]
|
||||
flattenMap:^(SQRLShipItState *state) {
|
||||
return waitForTerminationIfNecessary(state);
|
||||
}]
|
||||
then:^{
|
||||
// Read the latest state, in case it was modified by the
|
||||
// controlling application in the meantime.
|
||||
return [SQRLShipItState readUsingURL:stateLocation];
|
||||
}]
|
||||
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);
|
||||
|
||||
@@ -73,11 +75,14 @@ int main(int argc, const char * argv[]) {
|
||||
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:directoryManager];
|
||||
|
||||
NSUInteger attempt = (freshInstall ? 1 : state.installationStateAttempt + 1);
|
||||
RACSignal *action;
|
||||
|
||||
if (attempt > SQRLShipItMaximumInstallationAttempts) {
|
||||
return [[[installer.abortInstallationCommand
|
||||
execute:state]
|
||||
initially:^{
|
||||
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);
|
||||
@@ -86,9 +91,8 @@ int main(int argc, const char * argv[]) {
|
||||
return [RACSignal empty];
|
||||
}];
|
||||
} else {
|
||||
return [[[[[state
|
||||
writeUsingURL:stateLocation]
|
||||
initially:^{
|
||||
action = [[[[RACSignal
|
||||
defer:^{
|
||||
if (freshInstall) {
|
||||
NSLog(@"Beginning installation");
|
||||
state.installerState = SQRLInstallerStateClearingQuarantine;
|
||||
@@ -97,15 +101,39 @@ int main(int argc, const char * argv[]) {
|
||||
}
|
||||
|
||||
state.installationStateAttempt = attempt;
|
||||
|
||||
return [state writeUsingURL:stateLocation];
|
||||
}]
|
||||
then:^{
|
||||
return [installer.installUpdateCommand execute:state];
|
||||
}]
|
||||
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);
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
//
|
||||
// Squirrel-Constants.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 30/10/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Error domain for errors originating in Squirrel.
|
||||
extern NSString * const SQRLErrorDomain;
|
||||
|
||||
// Error codes for errors originating in Squirrel.
|
||||
//
|
||||
// SQRLCodeSignatureErrorDidNotPass - The bundle did not pass codesign
|
||||
// verification.
|
||||
//
|
||||
// SQRLCodeSignatureErrorCouldNotCreateStaticCode - A static code object could
|
||||
// not be created for the target bundle or running code.
|
||||
//
|
||||
// SQRLShipItStateErrorMissingRequiredProperty - A required property was `nil`
|
||||
// upon initialization. Includes `SQRLShipItStatePropertyErrorKey` in the
|
||||
// error's `userInfo` dictionary.
|
||||
//
|
||||
// SQRLShipItStateErrorUnarchiving - The saved state on disk could not be
|
||||
// unarchived, possibly because it's invalid.
|
||||
//
|
||||
// SQRLShipItStateErrorArchiving - The state object could not be archived.
|
||||
//
|
||||
// SQRLUpdaterErrorMissingUpdateBundle - The downloaded update does not contain
|
||||
// an app bundle, or it was deleted on disk before we could get to it.
|
||||
//
|
||||
// SQRLUpdaterErrorPreparingUpdateJob - An error occurred in the out-of-process
|
||||
// updater while it was setting up.
|
||||
//
|
||||
// SQRLUpdaterErrorRetrievingCodeSigningRequirement - The code signing
|
||||
// requirement for the running application could not be retrieved.
|
||||
//
|
||||
// SQRLUpdaterErrorInvalidServerResponse - The server sent a response that we
|
||||
// didn't understand. Includes `SQRLUpdaterServerDataErrorKey` in the error's
|
||||
// `userInfo` dictionary.
|
||||
//
|
||||
// SQRLUpdaterErrorInvalidJSON - The server sent update JSON that we didn't
|
||||
// understand. Includes `SQRLUpdaterJSONObjectErrorKey` in the error's
|
||||
// `userInfo` dictionary.
|
||||
//
|
||||
// SQRLZipArchiverShellTaskFailed - `SQRLZipArchiver` tried to invoke the shell
|
||||
// and failed. Includes `SQRLZipArchiverExitStatusErrorKey` in the error's
|
||||
// `userInfo` dictionary.
|
||||
//
|
||||
// SQRLShipItLauncherErrorCouldNotStartService - The ShipIt service could not be
|
||||
// started.
|
||||
typedef enum : NSInteger {
|
||||
SQRLCodeSignatureErrorDidNotPass = -1,
|
||||
SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2,
|
||||
|
||||
SQRLShipItStateErrorMissingRequiredProperty = -100,
|
||||
SQRLShipItStateErrorUnarchiving = -101,
|
||||
SQRLShipItStateErrorArchiving = -102,
|
||||
|
||||
SQRLUpdaterErrorMissingUpdateBundle = -200,
|
||||
SQRLUpdaterErrorPreparingUpdateJob = -201,
|
||||
SQRLUpdaterErrorRetrievingCodeSigningRequirement = -202,
|
||||
SQRLUpdaterErrorInvalidServerResponse = -203,
|
||||
SQRLUpdaterErrorInvalidJSON = -204,
|
||||
|
||||
SQRLZipArchiverShellTaskFailed = -300,
|
||||
|
||||
SQRLShipItLauncherErrorCouldNotStartService = -400,
|
||||
} SQRLErrorCode;
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// Squirrel-Constants.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 30/10/2013.
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
NSString * const SQRLErrorDomain = @"SQRLErrorDomain";
|
||||
@@ -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
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
SpecBegin(SQRLCodeSignature)
|
||||
|
||||
@@ -55,7 +54,7 @@ describe(@"code signature changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
|
||||
@@ -67,7 +66,7 @@ describe(@"code signature changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
});
|
||||
@@ -81,7 +80,7 @@ describe(@"main executable changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
|
||||
});
|
||||
|
||||
@@ -93,7 +92,7 @@ describe(@"main executable changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
});
|
||||
@@ -114,7 +113,7 @@ describe(@"helper executable changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
|
||||
@@ -126,7 +125,7 @@ describe(@"helper executable changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
});
|
||||
@@ -147,7 +146,7 @@ describe(@"resource changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
|
||||
@@ -159,7 +158,7 @@ describe(@"resource changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
});
|
||||
@@ -179,7 +178,7 @@ describe(@"framework changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
|
||||
@@ -191,7 +190,7 @@ describe(@"framework changes", ^{
|
||||
expect(success).to.beFalsy();
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
|
||||
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,12 +60,16 @@ void (^deepCodesignTestApplication)(void) = ^{
|
||||
|
||||
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];
|
||||
@@ -106,7 +110,7 @@ it(@"should deep sign the test application", ^{
|
||||
deepCodesignTestApplication();
|
||||
});
|
||||
|
||||
it(@"should deep verify after signing", ^{
|
||||
xit(@"should deep verify after signing", ^{
|
||||
expect(deepVerify()).to.beFalsy();
|
||||
deepCodesignTestApplication();
|
||||
expect(deepVerify()).to.beTruthy();
|
||||
|
||||
@@ -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
|
||||
@@ -77,7 +77,7 @@ it(@"should install an update in process", ^{
|
||||
expect(installer).notTo.beNil();
|
||||
|
||||
NSError *installError = nil;
|
||||
BOOL install = [[installer.installUpdateCommand execute:state] asynchronouslyWaitUntilCompleted:&installError];
|
||||
BOOL install = [[installer installUpdateWithState:state] asynchronouslyWaitUntilCompleted:&installError];
|
||||
expect(install).to.beTruthy();
|
||||
expect(installError).to.beNil();
|
||||
});
|
||||
@@ -104,6 +104,28 @@ it(@"should not install an update after too many attempts", ^{
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -117,6 +139,9 @@ describe(@"signal handling", ^{
|
||||
|
||||
[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.
|
||||
NSTimeInterval delay = arc4random_uniform(50) / 1000.0;
|
||||
@@ -136,26 +161,26 @@ describe(@"signal handling", ^{
|
||||
});
|
||||
|
||||
it(@"should handle SIGHUP", ^{
|
||||
system("killall -v -HUP ShipIt");
|
||||
system("killall -HUP ShipIt");
|
||||
});
|
||||
|
||||
it(@"should handle SIGTERM", ^{
|
||||
system("killall -v -TERM ShipIt");
|
||||
system("killall -TERM ShipIt");
|
||||
});
|
||||
|
||||
it(@"should handle SIGINT", ^{
|
||||
system("killall -v -INT ShipIt");
|
||||
system("killall -INT ShipIt");
|
||||
});
|
||||
|
||||
it(@"should handle SIGQUIT", ^{
|
||||
system("killall -v -QUIT ShipIt");
|
||||
system("killall -QUIT ShipIt");
|
||||
});
|
||||
|
||||
it(@"should handle SIGKILL", ^{
|
||||
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
|
||||
// a few times to really test resumption.
|
||||
for (int i = 0; i < 3; i++) {
|
||||
system("killall -v -KILL ShipIt");
|
||||
system("killall -KILL ShipIt");
|
||||
|
||||
// Wait at least for the launchd throttle interval.
|
||||
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
|
||||
|
||||
@@ -40,7 +40,8 @@ it(@"should wait until one instance terminates", ^{
|
||||
expect(observedApp).will.equal(app);
|
||||
expect(completed).to.beFalsy();
|
||||
|
||||
expect([app terminate]).to.beTruthy();
|
||||
[app forceTerminate];
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(completed).will.beTruthy();
|
||||
});
|
||||
|
||||
@@ -55,10 +56,12 @@ it(@"should wait until multiple instances terminate", ^{
|
||||
|
||||
expect(completed).to.beFalsy();
|
||||
|
||||
expect([app1 terminate]).to.beTruthy();
|
||||
[app1 forceTerminate];
|
||||
expect(app1.terminated).will.beTruthy();
|
||||
expect(completed).to.beFalsy();
|
||||
|
||||
expect([app2 terminate]).to.beTruthy();
|
||||
[app2 forceTerminate];
|
||||
expect(app2.terminated).will.beTruthy();
|
||||
expect(completed).will.beTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -88,4 +88,11 @@ extern NSString * const SQRLBundleShortVersionStringKey;
|
||||
// automatically be unmounted and deleted at the end of the example.
|
||||
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL;
|
||||
|
||||
// Add a block to cleanup after each example has run
|
||||
//
|
||||
// Blocks are run in reverse order, i.e. LIFO
|
||||
//
|
||||
// block - The block to invoke after the current example has finished
|
||||
- (void)addCleanupBlock:(dispatch_block_t)block;
|
||||
|
||||
@end
|
||||
|
||||
@@ -51,6 +51,10 @@ static void SQRLSignalHandler(int sig) {
|
||||
// all copied test data.
|
||||
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
|
||||
|
||||
// Returns an _unlaunched_ task that will follow log files at the given paths,
|
||||
// then pipe that output through this process.
|
||||
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SQRLTestCase
|
||||
@@ -71,27 +75,27 @@ static void SQRLSignalHandler(int sig) {
|
||||
@"otest-x86_64.ShipIt",
|
||||
];
|
||||
|
||||
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
|
||||
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
|
||||
return @[
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
|
||||
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
|
||||
].rac_sequence;
|
||||
}];
|
||||
|
||||
for (NSURL *URL in URLs) {
|
||||
[[NSData data] writeToURL:URL atomically:YES];
|
||||
}
|
||||
|
||||
NSArray *args = [[[URLs
|
||||
map:^(NSURL *URL) {
|
||||
return URL.path;
|
||||
RACSignal *logLocations = [[[[folders.rac_signal
|
||||
flattenMap:^(NSString *folder) {
|
||||
return [folder stringsByAppendingPaths:@[
|
||||
@"ShipIt_stdout.log",
|
||||
@"ShipIt_stderr.log",
|
||||
]].rac_signal;
|
||||
}]
|
||||
startWith:@"-f"]
|
||||
array];
|
||||
map:^(NSString *path) {
|
||||
return [appSupportURL URLByAppendingPathComponent:path];
|
||||
}]
|
||||
doNext:^(NSURL *location) {
|
||||
[[NSData data] writeToURL:location atomically:YES];
|
||||
}]
|
||||
map:^(NSURL *location) {
|
||||
return location.path;
|
||||
}];
|
||||
|
||||
NSTask *readShipIt = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:args];
|
||||
NSAssert([readShipIt isRunning], @"Could not start task %@ with arguments: %@", readShipIt, args);
|
||||
NSTask *readShipIt = [self tailTaskWithPaths:logLocations];
|
||||
[readShipIt launch];
|
||||
|
||||
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
|
||||
|
||||
atexit_b(^{
|
||||
[readShipIt terminate];
|
||||
@@ -131,6 +135,24 @@ static void SQRLSignalHandler(int sig) {
|
||||
[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 {
|
||||
@@ -176,7 +198,9 @@ static void SQRLSignalHandler(int sig) {
|
||||
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
|
||||
[[NSData data] writeToURL:testAppLog atomically:YES];
|
||||
|
||||
NSTask *readTestApp = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:@[ @"-f", testAppLog.path ]];
|
||||
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSignal return:testAppLog.path]];
|
||||
[readTestApp launch];
|
||||
|
||||
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
@@ -301,11 +325,7 @@ static void SQRLSignalHandler(int sig) {
|
||||
|
||||
[self addCleanupBlock:^{
|
||||
// Remove ShipIt's launchd job so it doesn't relaunch itself.
|
||||
CFErrorRef removeError = NULL;
|
||||
if (!SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, &removeError)) {
|
||||
NSLog(@"Could not remove ShipIt job after tests: %@", removeError);
|
||||
if (removeError != NULL) CFRelease(removeError);
|
||||
}
|
||||
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
|
||||
|
||||
NSError *lookupError = nil;
|
||||
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
|
||||
|
||||
@@ -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
|
||||
@@ -39,13 +39,17 @@ it(@"should validate release name and notes", ^{
|
||||
});
|
||||
|
||||
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];
|
||||
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];
|
||||
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();
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "OHHTTPStubs/OHHTTPStubs.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLURLConnection.h"
|
||||
#import "SQRLDownloadManager.h"
|
||||
|
||||
SpecBegin(SQRLUpdater)
|
||||
|
||||
@@ -51,10 +55,14 @@ beforeEach(^{
|
||||
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
|
||||
NSError *error = nil;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
} error:&error];
|
||||
expect(update).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
@@ -130,4 +138,127 @@ it(@"should use the application's bundled version of Squirrel and update in-plac
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"response handling", ^{
|
||||
__block NSURLRequest *localRequest = nil;
|
||||
__block SQRLUpdater *updater = nil;
|
||||
|
||||
before(^{
|
||||
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
|
||||
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
|
||||
});
|
||||
|
||||
it(@"should return an error for non 2xx code HTTP responses", ^{
|
||||
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
|
||||
return [request.URL isEqual:localRequest.URL];
|
||||
} withStubResponse:^(NSURLRequest *request) {
|
||||
return [OHHTTPStubsResponse responseWithData:nil statusCode:/* Server Error */ 500 responseTime:0 headers:nil];
|
||||
}];
|
||||
[self addCleanupBlock:^{
|
||||
[OHHTTPStubs removeRequestHandler:stubs];
|
||||
}];
|
||||
|
||||
NSError *error = nil;
|
||||
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", ^{
|
||||
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
|
||||
return [request.URL isEqual:localRequest.URL];
|
||||
} withStubResponse:^(NSURLRequest *request) {
|
||||
return [OHHTTPStubsResponse responseWithData:NSData.data statusCode:/* OK */ 200 responseTime:0 headers:nil];
|
||||
}];
|
||||
[self addCleanupBlock:^{
|
||||
[OHHTTPStubs removeRequestHandler:stubs];
|
||||
}];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(result).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
|
||||
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
|
||||
});
|
||||
});
|
||||
|
||||
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[@"update"];
|
||||
}]
|
||||
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;
|
||||
}];
|
||||
|
||||
launchWithEnvironment(nil);
|
||||
|
||||
expect(downloadedUpdate).willNot.beNil();
|
||||
|
||||
NSURL *bundleURL = downloadedUpdate.bundle.bundleURL;
|
||||
expect([bundleURL.path rangeOfString:@".noindex"].location).notTo.equal(NSNotFound);
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
SpecBegin(SQRLZipArchiver)
|
||||
SpecBegin(SQRLZipOperation)
|
||||
|
||||
it(@"should extract a zip archive created by the Finder", ^{
|
||||
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
|
||||
@@ -36,7 +35,7 @@ it(@"should fail to extract a nonexistent zip archive", ^{
|
||||
NSLog(@"%@", error);
|
||||
|
||||
expect(error).notTo.beNil();
|
||||
expect(error.domain).to.equal(SQRLErrorDomain);
|
||||
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
|
||||
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
|
||||
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLShipItLauncher.h"
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import "SQRLDownloadedUpdate.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
|
||||
@@ -70,18 +71,22 @@
|
||||
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
|
||||
updateCheckCount++;
|
||||
|
||||
return [self.updater.checkForUpdatesCommand execute:RACUnit.defaultUnit];
|
||||
return [self.updater.checkForUpdatesAction deferred];
|
||||
}]
|
||||
doNext:^(SQRLDownloadedUpdate *update) {
|
||||
NSLog(@"Got a candidate update: %@", update);
|
||||
|
||||
NSString *serialisedUpdate = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[MTLJSONAdapter JSONDictionaryFromModel:update] options:0 error:NULL] encoding:NSUTF8StringEncoding];
|
||||
|
||||
[NSDistributedNotificationCenter.defaultCenter postNotificationName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil userInfo:@{ @"update": serialisedUpdate }];
|
||||
}]
|
||||
// Retry until we get the expected release.
|
||||
repeat]
|
||||
skipUntilBlock:^(SQRLDownloadedUpdate *download) {
|
||||
skipWhile:^ BOOL (SQRLDownloadedUpdate *download) {
|
||||
SQRLTestUpdate *testUpdate = (id)download.update;
|
||||
NSAssert([testUpdate isKindOfClass:SQRLTestUpdate.class], @"Unexpected update type: %@", testUpdate);
|
||||
|
||||
return testUpdate.final;
|
||||
return !testUpdate.final;
|
||||
}]
|
||||
take:1]
|
||||
doNext:^(id _) {
|
||||
@@ -92,12 +97,12 @@
|
||||
NSLog(@"Error in updater: %@", error);
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
then:^{
|
||||
concat:[RACSignal defer:^{
|
||||
NSString *delayString = NSProcessInfo.processInfo.environment[@"SQRLUpdateDelay"];
|
||||
if (delayString == nil) return [RACSignal empty];
|
||||
|
||||
return [[RACSignal interval:delayString.doubleValue onScheduler:RACScheduler.mainThreadScheduler] take:1];
|
||||
}]
|
||||
}]]
|
||||
subscribeCompleted:^{
|
||||
[NSApp terminate:self];
|
||||
}];
|
||||
|
||||
+68
-6
@@ -1,6 +1,66 @@
|
||||
These scripts are primarily meant to support the use of
|
||||
[Janky](https://github.com/github/janky). To use them, read the contents of this
|
||||
repository into a `script` folder:
|
||||
# objc-build-scripts
|
||||
|
||||
This project is a collection of scripts created with two goals:
|
||||
|
||||
1. To standardize how Objective-C projects are bootstrapped after cloning
|
||||
1. To easily build Objective-C projects on continuous integration servers
|
||||
|
||||
## Scripts
|
||||
|
||||
Right now, there are two important scripts: [`bootstrap`](#bootstrap) and
|
||||
[`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and
|
||||
eliminate pesky system configuration issues (like setting up a working Ruby
|
||||
environment).
|
||||
|
||||
The structure of the scripts on disk is meant to follow that of a typical Ruby
|
||||
project:
|
||||
|
||||
```
|
||||
script/
|
||||
bootstrap
|
||||
cibuild
|
||||
```
|
||||
|
||||
### bootstrap
|
||||
|
||||
This script is responsible for bootstrapping (initializing) your project after
|
||||
it's been checked out. Here, you should install or clone any dependencies that
|
||||
are required for a working build and development environment.
|
||||
|
||||
By default, the script will verify that [xctool][] is installed, then initialize
|
||||
and update submodules recursively. If any submodules contain `script/bootstrap`,
|
||||
that will be run as well.
|
||||
|
||||
To check that other tools are installed, you can set the `REQUIRED_TOOLS`
|
||||
environment variable before running `script/bootstrap`, or edit it within the
|
||||
script directly. Note that no installation is performed automatically, though
|
||||
this can always be added within your specific project.
|
||||
|
||||
### cibuild
|
||||
|
||||
This script is responsible for building the project, as you would want it built
|
||||
for continuous integration. This is preferable to putting the logic on the CI
|
||||
server itself, since it ensures that any changes are versioned along with the
|
||||
source.
|
||||
|
||||
By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode
|
||||
workspace or project in the working directory, then build all targets/schemes
|
||||
(as found by `xcodebuild -list`) using [xctool][].
|
||||
|
||||
You can also specify the schemes to build by passing them into the script:
|
||||
|
||||
```sh
|
||||
script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS
|
||||
```
|
||||
|
||||
As with the `bootstrap` script, there are several environment variables that can
|
||||
be used to customize behavior. They can be set on the command line before
|
||||
invoking the script, or the defaults changed within the script directly.
|
||||
|
||||
## Getting Started
|
||||
|
||||
To add the scripts to your project, read the contents of this repository into
|
||||
a `script` folder:
|
||||
|
||||
```
|
||||
$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
|
||||
@@ -8,13 +68,15 @@ $ git fetch objc-build-scripts
|
||||
$ git read-tree --prefix=script/ -u objc-build-scripts/master
|
||||
```
|
||||
|
||||
Then commit the changes to incorporate the scripts into your own repository's
|
||||
Then commit the changes, to incorporate the scripts into your own repository's
|
||||
history. You can also freely tweak the scripts for your specific project's
|
||||
needs.
|
||||
|
||||
To bring in upstream changes later:
|
||||
To merge in upstream changes later:
|
||||
|
||||
```
|
||||
$ git fetch -p objc-build-scripts
|
||||
$ git merge -Xsubtree=script objc-build-scripts/master
|
||||
$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
|
||||
```
|
||||
|
||||
[xctool]: https://github.com/facebook/xctool
|
||||
|
||||
+69
-7
@@ -1,11 +1,73 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
cd "$SCRIPT_DIR/.."
|
||||
export SCRIPT_DIR=$(dirname "$0")
|
||||
|
||||
set -o errexit
|
||||
##
|
||||
## Configuration Variables
|
||||
##
|
||||
|
||||
echo "*** Updating submodules..."
|
||||
git submodule sync --quiet
|
||||
git submodule update --init
|
||||
git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init"
|
||||
config ()
|
||||
{
|
||||
# A whitespace-separated list of executables that must be present and locatable.
|
||||
: ${REQUIRED_TOOLS="xctool"}
|
||||
|
||||
export REQUIRED_TOOLS
|
||||
}
|
||||
|
||||
##
|
||||
## Bootstrap Process
|
||||
##
|
||||
|
||||
main ()
|
||||
{
|
||||
config
|
||||
|
||||
if [ -n "$REQUIRED_TOOLS" ]
|
||||
then
|
||||
echo "*** Checking dependencies..."
|
||||
check_deps
|
||||
fi
|
||||
|
||||
local submodules=$(git submodule status 2>/dev/null)
|
||||
if [ -n "$submodules" ]
|
||||
then
|
||||
echo "*** Updating submodules..."
|
||||
update_submodules
|
||||
fi
|
||||
}
|
||||
|
||||
check_deps ()
|
||||
{
|
||||
for tool in $REQUIRED_TOOLS
|
||||
do
|
||||
which -s "$tool"
|
||||
if [ "$?" -ne "0" ]
|
||||
then
|
||||
echo "*** Error: $tool not found. Please install it and bootstrap again."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
bootstrap_submodule ()
|
||||
{
|
||||
local bootstrap="script/bootstrap"
|
||||
|
||||
if [ -e "$bootstrap" ]
|
||||
then
|
||||
echo "*** Bootstrapping $name..."
|
||||
"$bootstrap" >/dev/null
|
||||
else
|
||||
update_submodules
|
||||
fi
|
||||
}
|
||||
|
||||
update_submodules ()
|
||||
{
|
||||
git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule
|
||||
}
|
||||
|
||||
export -f bootstrap_submodule
|
||||
export -f update_submodules
|
||||
|
||||
main
|
||||
|
||||
+113
-80
@@ -1,129 +1,162 @@
|
||||
#!/bin/bash
|
||||
|
||||
SCRIPT_DIR=$(dirname "$0")
|
||||
cd "$SCRIPT_DIR/.."
|
||||
export SCRIPT_DIR=$(dirname "$0")
|
||||
|
||||
##
|
||||
## Configuration Variables
|
||||
##
|
||||
|
||||
# The build configuration to use.
|
||||
if [ -z "$XCCONFIGURATION" ]
|
||||
then
|
||||
XCCONFIGURATION="Test"
|
||||
fi
|
||||
SCHEMES="$@"
|
||||
|
||||
# The workspace to build.
|
||||
#
|
||||
# If not set and no workspace is found, the -workspace flag will not be passed
|
||||
# to xcodebuild.
|
||||
if [ -z "$XCWORKSPACE" ]
|
||||
then
|
||||
XCWORKSPACE=$(ls -d *.xcworkspace 2>/dev/null | head -n 1)
|
||||
fi
|
||||
config ()
|
||||
{
|
||||
# The workspace to build.
|
||||
#
|
||||
# If not set and no workspace is found, the -workspace flag will not be passed
|
||||
# to `xctool`.
|
||||
#
|
||||
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
|
||||
# take precedence.
|
||||
: ${XCWORKSPACE=$(find_pattern "*.xcworkspace")}
|
||||
|
||||
# A bootstrap script to run before building.
|
||||
#
|
||||
# If this file does not exist, it is not considered an error.
|
||||
BOOTSTRAP="$SCRIPT_DIR/bootstrap"
|
||||
# The project to build.
|
||||
#
|
||||
# If not set and no project is found, the -project flag will not be passed
|
||||
# to `xctool`.
|
||||
#
|
||||
# Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will
|
||||
# take precedence.
|
||||
: ${XCODEPROJ=$(find_pattern "*.xcodeproj")}
|
||||
|
||||
# A whitespace-separated list of default targets or schemes to build, if none
|
||||
# are specified on the command line.
|
||||
#
|
||||
# Individual names can be quoted to avoid word splitting.
|
||||
DEFAULT_TARGETS=Squirrel
|
||||
# A bootstrap script to run before building.
|
||||
#
|
||||
# If this file does not exist, it is not considered an error.
|
||||
: ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"}
|
||||
|
||||
# Extra build settings to pass to xcodebuild.
|
||||
XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
|
||||
# Extra options to pass to xctool.
|
||||
: ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"}
|
||||
|
||||
# A whitespace-separated list of default schemes to build.
|
||||
#
|
||||
# Individual names can be quoted to avoid word splitting.
|
||||
: ${SCHEMES:=Squirrel}
|
||||
|
||||
# A source-able file with information about the Keychain, containing our
|
||||
# code-signing certificate.
|
||||
: ${KEYCHAIN_INFO:=/var/lib/jenkins/config/xcodekeychain}
|
||||
|
||||
export XCWORKSPACE
|
||||
export XCODEPROJ
|
||||
export BOOTSTRAP
|
||||
export XCTOOL_OPTIONS
|
||||
export SCHEMES
|
||||
export KEYCHAIN_INFO
|
||||
}
|
||||
|
||||
##
|
||||
## Code-signing Setup
|
||||
## Code Signing
|
||||
##
|
||||
|
||||
# A source-able file with information about the Keychain containing our
|
||||
# code-signing certificate.
|
||||
KEYCHAIN_INFO=/var/lib/jenkins/config/xcodekeychain
|
||||
|
||||
if [ -e "$KEYCHAIN_INFO" ]
|
||||
then
|
||||
# Unlock the keychain so the certificates it contains can be used for building.
|
||||
. "${KEYCHAIN_INFO}"
|
||||
security unlock-keychain -p "${XCODE_KEYCHAIN_PASSWORD}" "${XCODE_KEYCHAIN}"
|
||||
fi
|
||||
unlock_keychain ()
|
||||
{
|
||||
if [ -e "$KEYCHAIN_INFO" ]
|
||||
then
|
||||
# Unlock the keychain so the certificates it contains can be used for building.
|
||||
. "$KEYCHAIN_INFO"
|
||||
security unlock-keychain -p "$XCODE_KEYCHAIN_PASSWORD" "$XCODE_KEYCHAIN"
|
||||
fi
|
||||
}
|
||||
|
||||
##
|
||||
## Build Process
|
||||
##
|
||||
|
||||
if [ -z "$*" ]
|
||||
then
|
||||
# lol recursive shell script
|
||||
if [ -n "$DEFAULT_TARGETS" ]
|
||||
main ()
|
||||
{
|
||||
config
|
||||
unlock_keychain
|
||||
|
||||
if [ -f "$BOOTSTRAP" ]
|
||||
then
|
||||
echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
|
||||
else
|
||||
xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
|
||||
echo "*** Bootstrapping..."
|
||||
"$BOOTSTRAP" || exit $?
|
||||
fi
|
||||
|
||||
exit $?
|
||||
fi
|
||||
echo "*** The following schemes will be built:"
|
||||
echo "$SCHEMES" | xargs -n 1 echo " "
|
||||
echo
|
||||
|
||||
if [ -f "$BOOTSTRAP" ]
|
||||
then
|
||||
echo "*** Bootstrapping..."
|
||||
bash "$BOOTSTRAP" || exit $?
|
||||
fi
|
||||
echo "$SCHEMES" | xargs -n 1 | (
|
||||
local status=0
|
||||
|
||||
echo "*** The following targets will be built:"
|
||||
while read scheme
|
||||
do
|
||||
build_scheme "$scheme" || status=1
|
||||
done
|
||||
|
||||
for target in "$@"
|
||||
do
|
||||
echo "$target"
|
||||
done
|
||||
exit $status
|
||||
)
|
||||
}
|
||||
|
||||
echo "*** Cleaning all targets..."
|
||||
xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
|
||||
|
||||
run_xcodebuild ()
|
||||
find_pattern ()
|
||||
{
|
||||
local scheme=$1
|
||||
ls -d $1 2>/dev/null | head -n 1
|
||||
}
|
||||
|
||||
run_xctool ()
|
||||
{
|
||||
if [ -n "$XCWORKSPACE" ]
|
||||
then
|
||||
xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
|
||||
xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1
|
||||
elif [ -n "$XCODEPROJ" ]
|
||||
then
|
||||
xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1
|
||||
else
|
||||
xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
|
||||
echo "*** No workspace or project file found."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
local status=$?
|
||||
|
||||
return $status
|
||||
parse_build ()
|
||||
{
|
||||
awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null
|
||||
}
|
||||
|
||||
build_scheme ()
|
||||
{
|
||||
local scheme=$1
|
||||
|
||||
run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
|
||||
echo "*** Cleaning $scheme..."
|
||||
run_xctool -scheme "$scheme" clean >/dev/null || exit $?
|
||||
|
||||
echo "*** Building and testing $scheme..."
|
||||
echo
|
||||
|
||||
local sdkflag=
|
||||
local action=test
|
||||
|
||||
# Determine whether we can run unit tests for this target.
|
||||
run_xctool -scheme "$scheme" run-tests | parse_build
|
||||
|
||||
local awkstatus=$?
|
||||
local xcstatus=${PIPESTATUS[0]}
|
||||
|
||||
if [ "$xcstatus" -eq "65" ]
|
||||
if [ "$awkstatus" -ne "0" ]
|
||||
then
|
||||
# This probably means that there's no scheme by that name. Give up.
|
||||
echo "*** Error building scheme $scheme -- perhaps it doesn't exist"
|
||||
elif [ "$awkstatus" -eq "1" ]
|
||||
then
|
||||
return $awkstatus
|
||||
# Unit tests aren't supported.
|
||||
action=build
|
||||
fi
|
||||
|
||||
return $xcstatus
|
||||
if [ "$awkstatus" -eq "1" ]
|
||||
then
|
||||
# Build for iOS.
|
||||
sdkflag="-sdk iphonesimulator"
|
||||
fi
|
||||
|
||||
run_xctool $sdkflag -scheme "$scheme" $action
|
||||
}
|
||||
|
||||
echo "*** Building..."
|
||||
export -f build_scheme
|
||||
export -f run_xctool
|
||||
export -f parse_build
|
||||
|
||||
for scheme in "$@"
|
||||
do
|
||||
build_scheme "$scheme" || exit $?
|
||||
done
|
||||
main
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
BEGIN {
|
||||
FS = "\n";
|
||||
}
|
||||
|
||||
/Targets:/ {
|
||||
while (getline && $0 != "") {
|
||||
if ($0 ~ /Test/) continue;
|
||||
|
||||
sub(/^ +/, "");
|
||||
print "'" $0 "'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Exit statuses:
|
||||
#
|
||||
# 0 - No errors found.
|
||||
# 1 - Wrong SDK. Retry with SDK `iphonesimulator`.
|
||||
# 2 - Missing target.
|
||||
|
||||
BEGIN {
|
||||
status = 0;
|
||||
}
|
||||
|
||||
{
|
||||
print;
|
||||
}
|
||||
|
||||
/Testing with the '(.+)' SDK is not yet supported/ {
|
||||
status = 1;
|
||||
}
|
||||
|
||||
/does not contain a target named/ {
|
||||
status = 2;
|
||||
}
|
||||
|
||||
END {
|
||||
exit status;
|
||||
}
|
||||
Reference in New Issue
Block a user