Compare commits
116
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33137bcae6 | ||
|
|
532c565b2a | ||
|
|
9f5a0761ae | ||
|
|
572ef9aa9b | ||
|
|
b8aef91738 | ||
|
|
36e0531218 | ||
|
|
77aec66456 | ||
|
|
e8523fd74d | ||
|
|
767892c788 | ||
|
|
34e37569d7 | ||
|
|
4c93cb4508 | ||
|
|
431224c788 | ||
|
|
ea8f805825 | ||
|
|
7bb4c131e1 | ||
|
|
372a7d783a | ||
|
|
b85e03fbe9 | ||
|
|
48455f1ee4 | ||
|
|
5cd5fdb2ed | ||
|
|
aa545ed419 | ||
|
|
3d507a58a6 | ||
|
|
406728c066 | ||
|
|
53adc17554 | ||
|
|
0fb9ed48c9 | ||
|
|
1db648059a | ||
|
|
fccf29122d | ||
|
|
502c96a6df | ||
|
|
edb4aafa31 | ||
|
|
90a6a2fe16 | ||
|
|
c2869a05e9 | ||
|
|
de276d73a6 | ||
|
|
7655bd0a90 | ||
|
|
e9a5874339 | ||
|
|
aa69586334 | ||
|
|
8f3f5a5a72 | ||
|
|
f48ccd2a21 | ||
|
|
1ac9c4ddc1 | ||
|
|
9907d44f40 | ||
|
|
e21fd640b9 | ||
|
|
7bb7799b17 | ||
|
|
b5bbdfbed0 | ||
|
|
28079454dc | ||
|
|
210e09e079 | ||
|
|
f0161a51d9 | ||
|
|
d121f4cf7f | ||
|
|
02bb683cf7 | ||
|
|
d64a09bb2b | ||
|
|
5f201f833e | ||
|
|
6e1b3c9ded | ||
|
|
13a35d195b | ||
|
|
0c95e0c8c4 | ||
|
|
656decb128 | ||
|
|
4ef110e84b | ||
|
|
a22ebbfdb9 | ||
|
|
8cdf3d0666 | ||
|
|
ff6a2e1cb7 | ||
|
|
c3a41e9032 | ||
|
|
27eb479de4 | ||
|
|
aab4ca3efb | ||
|
|
6bc3b0d466 | ||
|
|
ef83ed3175 | ||
|
|
c382fbce40 | ||
|
|
d0498c528e | ||
|
|
e5c65e530f | ||
|
|
65f191ce30 | ||
|
|
cb2502510b | ||
|
|
49a8f3b624 | ||
|
|
36e3b17abb | ||
|
|
eb49de653b | ||
|
|
35e5a03130 | ||
|
|
b4387c312a | ||
|
|
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 | ||
|
|
72d768d17d | ||
|
|
91549567e1 | ||
|
|
6e9efa0be3 | ||
|
|
8709338949 | ||
|
|
f9d6c00f60 | ||
|
|
e122927c5d | ||
|
|
43d524e186 | ||
|
|
a4dc3e4a47 | ||
|
|
e5f838463b | ||
|
|
e24d538232 | ||
|
|
9170c4d106 | ||
|
|
a93cf336b8 | ||
|
|
7bc6620b45 | ||
|
|
4f0ba2c41a | ||
|
|
cfaabf7cb0 | ||
|
|
d4cd9fa88c | ||
|
|
a7a3ddbf9c |
+4
-1
@@ -3,4 +3,7 @@
|
||||
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
|
||||
[submodule "External/Mantle"]
|
||||
path = External/Mantle
|
||||
url = https://github.com/github/Mantle.git
|
||||
url = https://github.com/MantleFramework/Mantle.git
|
||||
[submodule "External/OHHTTPStubs"]
|
||||
path = External/OHHTTPStubs
|
||||
url = https://github.com/github/OHHTTPStubs
|
||||
|
||||
Vendored
+1
-1
Submodule External/Mantle updated: 6cbdbebc44...8aef509d18
+1
Submodule External/OHHTTPStubs added at acaf8b8af7
Vendored
+1
-1
Submodule External/ReactiveCocoa updated: c01fe91544...09ed195916
@@ -19,15 +19,40 @@ 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. Install xctool with `brew install xctool`
|
||||
1. Add the Squirrel repository as a git submodule
|
||||
1. Run `script/bootstrap` from within the submodule
|
||||
1. Add a reference to Squirrel.xcodeproj to your project
|
||||
1. Add Squirrel.framework as a target dependency
|
||||
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
|
||||
it into your Frameworks directory
|
||||
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
|
||||
Frameworks directory Squirrel.framework is copied into
|
||||
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
|
||||
does not embed them itself.
|
||||
|
||||
# Dependencies
|
||||
|
||||
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
|
||||
and [Mantle](https://github.com/MantleFramework/Mantle).
|
||||
|
||||
If your application is already using ReactiveCocoa, ensure it is using the same
|
||||
version as Squirrel.
|
||||
|
||||
Otherwise, add a target dependency and Copy Files build phase entry for the
|
||||
ReactiveCocoa.framework target included in Squirrel's repository, in
|
||||
External/ReactiveCocoa.
|
||||
|
||||
Similarly, ensure your application includes Mantle, or copies in the Squirrel
|
||||
version.
|
||||
|
||||
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
|
||||
includes the directory that Squirrel.framework, ReactiveCocoa.framework
|
||||
and Mantle.framework are copied into.
|
||||
|
||||
# Configuration
|
||||
|
||||
Once Squirrel is added to your project, you need to configure and start it.
|
||||
|
||||
@@ -36,30 +61,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 +133,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).
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; };
|
||||
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; };
|
||||
53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; };
|
||||
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */ = {isa = PBXBuildFile; fileRef = 5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */; };
|
||||
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */ = {isa = PBXBuildFile; fileRef = 5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */; };
|
||||
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
|
||||
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 */; };
|
||||
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */; };
|
||||
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 +109,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 */;
|
||||
@@ -393,11 +421,12 @@
|
||||
53710D8117D8F5C300A992DE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = "<group>"; };
|
||||
53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = "<group>"; };
|
||||
53710D8317D8F5C300A992DE /* xcodebuild.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = xcodebuild.awk; sourceTree = "<group>"; };
|
||||
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLAuthorization.h; sourceTree = "<group>"; };
|
||||
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLAuthorization.m; sourceTree = "<group>"; };
|
||||
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -545,6 +574,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 +626,23 @@
|
||||
path = script;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
53A603F7182134D3002DB2C7 /* Supporting Files */ = {
|
||||
5374DCD8187AD0DC006B7056 /* Authorization */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */,
|
||||
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */,
|
||||
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */,
|
||||
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
name = Authorization;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
53B70015181E944F000FFBD0 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */,
|
||||
53B70023181E944F000FFBD0 /* UnitTests.octest */,
|
||||
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D00F5B8217E82CFB009A4818 /* Extensions */ = {
|
||||
@@ -780,6 +820,7 @@
|
||||
children = (
|
||||
D097B9BC17BB4777006C3FEB /* IOKit.framework */,
|
||||
D014ABB917B97403007D79D0 /* ServiceManagement.framework */,
|
||||
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */,
|
||||
D0F3F01417E83918003AFA6B /* Expecta.xcodeproj */,
|
||||
D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */,
|
||||
D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */,
|
||||
@@ -940,7 +981,7 @@
|
||||
D0A56BF61804BABB00A84EDC /* Code Signing */,
|
||||
D0A56BE71804BA2400A84EDC /* State */,
|
||||
D06B58BC18032BE800656D97 /* Transactions */,
|
||||
53A603F7182134D3002DB2C7 /* Supporting Files */,
|
||||
5374DCD8187AD0DC006B7056 /* Authorization */,
|
||||
);
|
||||
name = Shared;
|
||||
path = Squirrel;
|
||||
@@ -989,12 +1030,12 @@
|
||||
isa = PBXHeadersBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */,
|
||||
D0964B3C17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.h in Headers */,
|
||||
D0C22C4C179CC15100158214 /* SQRLUpdater.h in Headers */,
|
||||
D0C22C52179CC23900158214 /* Squirrel.h in Headers */,
|
||||
D0964B3817F2E01500D88BF7 /* SQRLDownloadedUpdate.h in Headers */,
|
||||
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */,
|
||||
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */,
|
||||
D00F5B8B17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1089,11 +1130,11 @@
|
||||
D0C22BEA179CC00E00158214 /* Sources */,
|
||||
D0C22BEB179CC00E00158214 /* Frameworks */,
|
||||
D0C22BEC179CC00E00158214 /* Resources */,
|
||||
D0C22BED179CC00E00158214 /* ShellScript */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
53B7002F181E9456000FFBD0 /* PBXTargetDependency */,
|
||||
D0F3F03317E83924003AFA6B /* PBXTargetDependency */,
|
||||
D0F3F03517E83924003AFA6B /* PBXTargetDependency */,
|
||||
D08D4E4C17B4520C0012B22D /* PBXTargetDependency */,
|
||||
@@ -1134,6 +1175,10 @@
|
||||
ProductGroup = D0A56B971804B1FD00A84EDC /* Products */;
|
||||
ProjectRef = D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 53B70015181E944F000FFBD0 /* Products */;
|
||||
ProjectRef = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = D0F3F04B17E83A1D003AFA6B /* Products */;
|
||||
ProjectRef = D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */;
|
||||
@@ -1155,6 +1200,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 +1406,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;
|
||||
@@ -1375,7 +1425,6 @@
|
||||
D014AC0517B97885007D79D0 /* ShipIt-main.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 */,
|
||||
@@ -1406,8 +1455,8 @@
|
||||
D0EDBE9D17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m in Sources */,
|
||||
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
|
||||
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
|
||||
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
|
||||
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
|
||||
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */,
|
||||
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1441,6 +1490,11 @@
|
||||
target = 534FF35F17D8E90A0020A51A /* TestService */;
|
||||
targetProxy = 534FF37017D8E9170020A51A /* PBXContainerItemProxy */;
|
||||
};
|
||||
53B7002F181E9456000FFBD0 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
name = OHHTTPStubs;
|
||||
targetProxy = 53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */;
|
||||
};
|
||||
D014AC1717B9793A007D79D0 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = D014AC0017B97885007D79D0 /* ShipIt */;
|
||||
@@ -1591,6 +1645,7 @@
|
||||
buildSettings = {
|
||||
OTHER_LDFLAGS = "-all_load";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -1600,6 +1655,7 @@
|
||||
buildSettings = {
|
||||
OTHER_LDFLAGS = "-all_load";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -1609,6 +1665,7 @@
|
||||
buildSettings = {
|
||||
OTHER_LDFLAGS = "-all_load";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Test;
|
||||
};
|
||||
@@ -1618,6 +1675,7 @@
|
||||
buildSettings = {
|
||||
OTHER_LDFLAGS = "-all_load";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// SQRLAuthorization.h
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 06/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#import <Security/Security.h>
|
||||
|
||||
// Wraps an AuthorizationRef.
|
||||
@interface SQRLAuthorization : NSObject
|
||||
|
||||
// Designated initializer.
|
||||
//
|
||||
// authorization - Must not be NULL, the returned object assumes ownership of
|
||||
// the passed authorization.
|
||||
//
|
||||
// Returns an object which ties the `authorization` argument to the returned
|
||||
// object's lifetime.
|
||||
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
|
||||
|
||||
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// SQRLAuthorization.m
|
||||
// Squirrel
|
||||
//
|
||||
// Created by Keith Duncan on 06/01/2014.
|
||||
// Copyright (c) 2014 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLAuthorization.h"
|
||||
|
||||
@implementation SQRLAuthorization
|
||||
|
||||
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
|
||||
NSParameterAssert(authorization != NULL);
|
||||
|
||||
self = [self init];
|
||||
if (self == nil) return nil;
|
||||
|
||||
_authorization = authorization;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
+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.allObjects.rac_signal;
|
||||
}]
|
||||
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
|
||||
|
||||
@@ -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,11 @@
|
||||
#import <Security/Security.h>
|
||||
#import <ServiceManagement/ServiceManagement.h>
|
||||
#import <launch.h>
|
||||
#import "Squirrel-Constants.h"
|
||||
#import "SQRLAuthorization.h"
|
||||
|
||||
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
|
||||
|
||||
const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
|
||||
|
||||
@implementation SQRLShipItLauncher
|
||||
|
||||
@@ -68,7 +72,7 @@
|
||||
|
||||
+ (RACSignal *)shipItAuthorization {
|
||||
return [[RACSignal
|
||||
createSignal:^(id<RACSubscriber> subscriber) {
|
||||
create:^(id<RACSubscriber> subscriber) {
|
||||
AuthorizationItem rightItems[] = {
|
||||
{
|
||||
.name = kSMRightModifySystemDaemons,
|
||||
@@ -105,16 +109,13 @@
|
||||
|
||||
AuthorizationRef authorization = NULL;
|
||||
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
|
||||
|
||||
if (authorizationError == noErr) {
|
||||
[subscriber sendNext:(__bridge id)authorization];
|
||||
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
|
||||
[subscriber sendCompleted];
|
||||
} else {
|
||||
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
|
||||
}
|
||||
|
||||
return [RACDisposable disposableWithBlock:^{
|
||||
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
|
||||
}];
|
||||
}]
|
||||
setNameWithFormat:@"+shipItAuthorization"];
|
||||
}
|
||||
@@ -124,22 +125,22 @@
|
||||
zip:@[
|
||||
self.shipItJobDictionary,
|
||||
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
|
||||
] reduce:^(NSDictionary *jobDictionary, id authorization) {
|
||||
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
|
||||
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
|
||||
|
||||
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
|
||||
AuthorizationRef authorization = authorizationValue.authorization;
|
||||
|
||||
if (cfError != NULL) {
|
||||
CFRelease(cfError);
|
||||
cfError = NULL;
|
||||
CFErrorRef cfError;
|
||||
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
|
||||
NSError *error = CFBridgingRelease(cfError);
|
||||
cfError = NULL;
|
||||
|
||||
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
|
||||
NSLog(@"Could not remove previous ShipIt job: %@", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
|
||||
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
|
||||
return [RACSignal error:CFBridgingRelease(cfError)];
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
+16
-2
@@ -134,19 +134,33 @@ 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;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
+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 subscribing to this signal, the receiver is responsible for terminating
|
||||
// the application upon success. The app must not be terminated in any other way
|
||||
// unless an error occurs.
|
||||
//
|
||||
// Returns a signal that will error on the main scheduler if anything goes
|
||||
// wrong before termination. The signal will never complete.
|
||||
- (RACSignal *)relaunchToInstallUpdate;
|
||||
|
||||
@end
|
||||
|
||||
@interface SQRLUpdater (Unavailable)
|
||||
|
||||
+272
-100
@@ -20,11 +20,22 @@
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import <ReactiveCocoa/EXTScope.h>
|
||||
#import <ReactiveCocoa/ReactiveCocoa.h>
|
||||
#import "Squirrel-Constants.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;
|
||||
|
||||
// The prefix used when creating temporary directories for updates. This will be
|
||||
// followed by a random string of characters.
|
||||
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
|
||||
|
||||
@interface SQRLUpdater ()
|
||||
|
||||
// The code signature for the running application, used to check updates before
|
||||
@@ -36,6 +47,16 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
// Sends completed or error.
|
||||
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
|
||||
|
||||
// Lazily removes outdated temporary directories (used for previous updates)
|
||||
// upon first subscription.
|
||||
//
|
||||
// Pruning directories while an update is pending or in progress will result in
|
||||
// undefined behavior.
|
||||
//
|
||||
// Sends each removed directory then completes, or errors, on an unspecified
|
||||
// thread.
|
||||
@property (nonatomic, strong, readonly) RACSignal *prunedUpdateDirectories;
|
||||
|
||||
// Parses an update model from downloaded data.
|
||||
//
|
||||
// data - JSON data representing an update manifest. This must not be nil.
|
||||
@@ -103,16 +124,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
|
||||
@@ -133,37 +165,68 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
|
||||
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);
|
||||
if (_signature == nil) {
|
||||
#if DEBUG
|
||||
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
|
||||
return nil;
|
||||
#else
|
||||
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
|
||||
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
|
||||
#endif
|
||||
}
|
||||
|
||||
@weakify(self);
|
||||
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
|
||||
@strongify(self);
|
||||
NSParameterAssert(self.updateRequest != nil);
|
||||
|
||||
// TODO: Maybe allow this to be an argument to the command?
|
||||
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
|
||||
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
|
||||
_prunedUpdateDirectories = [[[[[[[[RACSignal
|
||||
defer:^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *appSupportURL) {
|
||||
NSFileManager *manager = [[NSFileManager alloc] init];
|
||||
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:appSupportURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
|
||||
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, appSupportURL, error);
|
||||
return YES;
|
||||
}];
|
||||
|
||||
return [[[[[[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];
|
||||
}];
|
||||
return [[enumerator.allObjects.rac_signal
|
||||
filter:^(NSURL *enumeratedURL) {
|
||||
NSString *name = enumeratedURL.lastPathComponent;
|
||||
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
|
||||
}]
|
||||
doNext:^(NSURL *directoryURL) {
|
||||
NSError *error = nil;
|
||||
if (![manager removeItemAtURL:directoryURL error:&error]) {
|
||||
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
collect]
|
||||
// These operators ensure that the actual work only executes once,
|
||||
// without starting it immediately.
|
||||
concat:[RACSignal never]]
|
||||
shareWhileActive]
|
||||
take:1]
|
||||
flattenMap:^(NSArray *URLs) {
|
||||
return URLs.rac_signal;
|
||||
}]
|
||||
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
|
||||
|
||||
_shipItLauncher = [[[RACSignal
|
||||
_checkForUpdatesAction = [[[RACSignal
|
||||
defer:^{
|
||||
@strongify(self);
|
||||
return [RACSignal return:self.updateRequest];
|
||||
}]
|
||||
flattenMap:^(NSURLRequest *request) {
|
||||
return [self checkForUpdates:request];
|
||||
}]
|
||||
action];
|
||||
|
||||
_updates = [[RACSubject
|
||||
subject]
|
||||
setNameWithFormat:@"%@ updates", self];
|
||||
|
||||
_shipItLauncher = [[[[[[[RACSignal
|
||||
defer:^{
|
||||
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
|
||||
|
||||
@@ -175,31 +238,90 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
// wait for another, more canonical error.
|
||||
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
|
||||
}]
|
||||
replayLazily]
|
||||
// These operators ensure that the actual work only executes once,
|
||||
// without starting it immediately. Super hacky.
|
||||
concat:[RACSignal return:RACUnit.defaultUnit]]
|
||||
concat:[RACSignal never]]
|
||||
shareWhileActive]
|
||||
take:1]
|
||||
ignoreValues]
|
||||
setNameWithFormat:@"shipItLauncher"];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (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
|
||||
signalWithValue:nil]
|
||||
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 [[[[[[[[self.prunedUpdateDirectories
|
||||
catch:^(NSError *error) {
|
||||
NSLog(@"Error pruning old updates: %@", error);
|
||||
return [RACSignal empty];
|
||||
}]
|
||||
ignoreValues]
|
||||
concat:[NSURLConnection rac_sendAsynchronousRequest:newRequest]]
|
||||
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
|
||||
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
|
||||
NSHTTPURLResponse *httpResponse = (id)response;
|
||||
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
|
||||
NSDictionary *errorInfo = @{
|
||||
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
|
||||
SQRLUpdaterServerDataErrorKey: bodyData,
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
|
||||
if (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 +338,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 +355,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];
|
||||
@@ -272,11 +394,25 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
|
||||
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
|
||||
|
||||
return [[[NSURLConnection
|
||||
return [[[[NSURLConnection
|
||||
rac_sendAsynchronousRequest:zipDownloadRequest]
|
||||
reduceEach:^(id _, NSData *data) {
|
||||
return data;
|
||||
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 download failed", nil),
|
||||
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
|
||||
SQRLUpdaterServerDataErrorKey: bodyData,
|
||||
};
|
||||
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
|
||||
return [RACSignal error:error];
|
||||
}
|
||||
}
|
||||
|
||||
return [RACSignal return:bodyData];
|
||||
}]
|
||||
flatten]
|
||||
flattenMap:^(NSData *data) {
|
||||
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
|
||||
|
||||
@@ -292,11 +428,16 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
NSLog(@"Download completed to: %@", zipOutputURL);
|
||||
}]
|
||||
flattenMap:^(NSURL *zipOutputURL) {
|
||||
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
|
||||
}]
|
||||
then:^{
|
||||
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
|
||||
return [[SQRLZipArchiver
|
||||
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
|
||||
doCompleted:^{
|
||||
NSError *error = nil;
|
||||
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
|
||||
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
|
||||
}
|
||||
}];
|
||||
}]
|
||||
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
|
||||
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
|
||||
}
|
||||
|
||||
@@ -309,7 +450,7 @@ NSString * const SQRLUpdaterJSONObjectErrorKey = @"SQRLUpdaterJSONObjectErrorKey
|
||||
return [directoryManager applicationSupportURL];
|
||||
}]
|
||||
flattenMap:^(NSURL *appSupportURL) {
|
||||
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
|
||||
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
|
||||
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
|
||||
@onExit {
|
||||
free(updateDirectoryCString);
|
||||
@@ -342,25 +483,27 @@ 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.allObjects.rac_signal
|
||||
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 +512,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 +521,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 +538,73 @@ 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]]
|
||||
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";
|
||||
@@ -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();
|
||||
|
||||
@@ -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];
|
||||
|
||||
+173
-71
@@ -6,9 +6,12 @@
|
||||
// Copyright (c) 2013 GitHub. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import "SQRLDirectoryManager.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
|
||||
#import "SQRLTestUpdate.h"
|
||||
#import "OHHTTPStubs/OHHTTPStubs.h"
|
||||
|
||||
SpecBegin(SQRLUpdater)
|
||||
|
||||
__block NSURL *JSONURL;
|
||||
@@ -49,85 +52,184 @@ beforeEach(^{
|
||||
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
|
||||
});
|
||||
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
describe(@"updating", ^{
|
||||
__block NSURL *updateURL;
|
||||
|
||||
beforeEach(^{
|
||||
updateURL = [self createTestApplicationUpdate];
|
||||
});
|
||||
|
||||
writeUpdate(update);
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
|
||||
NSError *error = nil;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:&error];
|
||||
expect(update).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should not install a corrupt update", ^{
|
||||
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
|
||||
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
|
||||
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
|
||||
// Give the update some time to finish installing.
|
||||
[NSThread sleepForTimeInterval:0.2];
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should update to the most recently enqueued job", ^{
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(self.testApplicationURL)
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
|
||||
// Now that Test Application is launched, it's going to keep checking the
|
||||
// JSON URL until it has the proper release name. So we'll wait a short bit,
|
||||
// and then add the correct name in.
|
||||
//
|
||||
// This exercises ShipIt's ability to discard previous commands and
|
||||
// install an even newer update.
|
||||
[NSThread sleepForTimeInterval:0.3];
|
||||
|
||||
update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSTimeInterval delay = 30;
|
||||
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
|
||||
|
||||
Expecta.asynchronousTestTimeout = delay + 3;
|
||||
expect(app.terminated).will.beTruthy();
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
describe(@"cleaning up", ^{
|
||||
__block NSURL *appSupportURL;
|
||||
__block RACSignal *updateDirectoryURLs;
|
||||
|
||||
beforeEach(^{
|
||||
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
|
||||
|
||||
appSupportURL = [[directoryManager applicationSupportURL] first];
|
||||
expect(appSupportURL).notTo.beNil();
|
||||
|
||||
updateDirectoryURLs = [[RACSignal
|
||||
defer:^{
|
||||
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
|
||||
if (contents == nil) return [RACSignal empty];
|
||||
|
||||
return contents.rac_signal;
|
||||
}]
|
||||
filter:^(NSURL *directoryURL) {
|
||||
return [directoryURL.lastPathComponent hasPrefix:@"update."];
|
||||
}];
|
||||
});
|
||||
|
||||
it(@"should remove downloaded archives after updating", ^{
|
||||
NSError *error = nil;
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:&error];
|
||||
|
||||
expect(update).notTo.beNil();
|
||||
expect(error).to.beNil();
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
|
||||
// Also test that this is the only update directory (any others
|
||||
// should've been removed).
|
||||
NSArray *directoryURLs = [updateDirectoryURLs array];
|
||||
expect(directoryURLs.count).to.equal(1);
|
||||
|
||||
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
|
||||
expect(contents).to.equal(@[]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(@"should not install a corrupt update", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
|
||||
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
|
||||
describe(@"response handling", ^{
|
||||
__block NSURLRequest *localRequest = nil;
|
||||
__block SQRLUpdater *updater = nil;
|
||||
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
before(^{
|
||||
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
|
||||
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
|
||||
});
|
||||
|
||||
writeUpdate(update);
|
||||
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];
|
||||
}];
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
expect(app.terminated).will.beTruthy();
|
||||
NSError *error = nil;
|
||||
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(result).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
|
||||
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
|
||||
});
|
||||
|
||||
// Give the update some time to finish installing.
|
||||
[NSThread sleepForTimeInterval:0.2];
|
||||
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
|
||||
});
|
||||
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];
|
||||
}];
|
||||
|
||||
it(@"should update to the most recently enqueued job", ^{
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(self.testApplicationURL)
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSRunningApplication *app = launchWithEnvironment(nil);
|
||||
|
||||
// Now that Test Application is launched, it's going to keep checking the
|
||||
// JSON URL until it has the proper release name. So we'll wait a short bit,
|
||||
// and then add the correct name in.
|
||||
//
|
||||
// This exercises ShipIt's ability to discard previous commands and
|
||||
// install an even newer update.
|
||||
[NSThread sleepForTimeInterval:0.3];
|
||||
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
expect(app.terminated).will.beTruthy();
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
});
|
||||
|
||||
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
|
||||
NSURL *updateURL = [self createTestApplicationUpdate];
|
||||
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
|
||||
@"updateURL": zipUpdate(updateURL),
|
||||
@"final": @YES
|
||||
} error:NULL];
|
||||
|
||||
writeUpdate(update);
|
||||
|
||||
NSTimeInterval delay = 30;
|
||||
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
|
||||
|
||||
Expecta.asynchronousTestTimeout = delay + 3;
|
||||
expect(app.terminated).will.beTruthy();
|
||||
|
||||
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
|
||||
NSError *error = nil;
|
||||
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
|
||||
expect(result).to.beFalsy();
|
||||
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
|
||||
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
|
||||
});
|
||||
});
|
||||
|
||||
SpecEnd
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#import "SQRLCodeSignature.h"
|
||||
#import "SQRLZipArchiver.h"
|
||||
#import "Squirrel-Constants.h"
|
||||
|
||||
SpecBegin(SQRLZipArchiver)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -70,18 +70,18 @@
|
||||
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
|
||||
updateCheckCount++;
|
||||
|
||||
return [self.updater.checkForUpdatesCommand execute:RACUnit.defaultUnit];
|
||||
return [self.updater.checkForUpdatesAction signalWithValue:nil];
|
||||
}]
|
||||
doNext:^(SQRLDownloadedUpdate *update) {
|
||||
NSLog(@"Got a candidate update: %@", update);
|
||||
}]
|
||||
// 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 +92,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