Author SHA1 Message Date
Keith Duncan 9b12e8faa7 Move all the error codes into one domain 2013-10-30 13:03:04 +00:00
Keith Duncan e8c0e4748e Add Squirrel-Constants for SQRLErrorDomain 2013-10-30 12:38:11 +00:00
65 changed files with 655 additions and 2929 deletions
-3
View File
@@ -4,6 +4,3 @@
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/github/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
Submodule External/OHHTTPStubs deleted from acaf8b8af7
+20 -29
View File
@@ -19,8 +19,6 @@ Support](#server-support) below.
Squirrel’s installer is also designed to be fault tolerant, and ensure that any
updates installed are valid.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
# Adopting Squirrel
1. Add the Squirrel repository as a git submodule
@@ -38,38 +36,30 @@ 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"]]];
// Check for updates every 4 hours.
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
[self.updater startAutomaticChecksWithInterval:/* 4 Hours */ 60 * 60 * 4];
[self.updater checkForUpdates];
}
```
Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
Squirrel will periodically request and automatically download any updates.
## Update Notifications
To know when an update is ready to be installed, you can subscribe to the
`updates` signal on `SQRLUpdater`:
Before your application terminates, it should tell Squirrel to install any
updates that it has downloaded:
```objc
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
NSLog(@"An update is ready to install: %@", downloadedUpdate);
}];
```
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
[self.updater installUpdateIfNeeded:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"Successfully installed an update");
} else {
NSLog(@"Failed to update: %@", error);
}
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.
[sender replyToApplicationShouldTerminate:YES];
}];
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);
}];
return NSTerminateLater;
}
```
# Update JSON Format
@@ -110,6 +100,7 @@ will check for an update again at the interval you specify.
# User Interface
Squirrel does not provide any GUI components for presenting updates. If you want
to indicate updates to the user, make sure to [listen for downloaded
updates](#update-notifications).
Squirrel does not provide an updates interface, if you want to display available
updates, subscribe to the `SQRLUpdaterUpdateAvailableNotification` notification.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
+32 -169
View File
@@ -7,42 +7,20 @@
objects = {
/* Begin PBXBuildFile section */
5316AF7A188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */; };
5316AF7B188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */; };
5316AF95188EFFA100C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */; };
5316AFA8188F002100C070DF /* SQRLDownload+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AFA6188F002100C070DF /* SQRLDownload+Private.h */; };
5316AFAC188F0A8E00C070DF /* NSData+SQRLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */; };
5316AFAD188F0A8E00C070DF /* NSData+SQRLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */; };
533076EB17EB688300BDCCE0 /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D097B9BC17BB4777006C3FEB /* IOKit.framework */; };
53390936184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */; };
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */; };
5339094A184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */; };
534FF35B17D8E8B90020A51A /* unused-helper in Copy LoginItems */ = {isa = PBXBuildFile; fileRef = D0B1DDCB17B487D90059C355 /* unused-helper */; };
534FF36117D8E90A0020A51A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F60CA7EA179FC4F60069F69A /* Foundation.framework */; };
534FF36717D8E90A0020A51A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 534FF36517D8E90A0020A51A /* InfoPlist.strings */; };
534FF36A17D8E90A0020A51A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 534FF36917D8E90A0020A51A /* main.m */; };
534FF37317D8E9370020A51A /* com.github.Squirrel.TestApplication.TestService.xpc in Copy XPCServices */ = {isa = PBXBuildFile; fileRef = 534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */; };
53542FC0184F86EC003A6AFD /* Mantle.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D0A56BA41804B1FD00A84EDC /* Mantle.framework */; };
53542FCE184F86F1003A6AFD /* ReactiveCocoa.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = D0F3F05617E83A1D003AFA6B /* ReactiveCocoa.framework */; };
53542FCF184F870C003A6AFD /* Mantle.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0A56BA41804B1FD00A84EDC /* Mantle.framework */; };
53542FD2184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */; };
536CD4E2183E5D7F000C0FD1 /* SQRLDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = 536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */; };
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */; };
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */; };
53710D8417D8F5CB00A992DE /* deep-codesign in Resources */ = {isa = PBXBuildFile; fileRef = 53710D7F17D8F5C300A992DE /* deep-codesign */; };
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A60406182134F9002DB2C7 /* Squirrel-Constants.h */; settings = {ATTRIBUTES = (Public, ); }; };
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A60407182134F9002DB2C7 /* Squirrel-Constants.m */; };
53A6040A182134F9002DB2C7 /* Squirrel-Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = 53A60407182134F9002DB2C7 /* Squirrel-Constants.m */; };
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = 53AACF4917E9CA0500B41027 /* SQRLUpdate.h */; settings = {ATTRIBUTES = (Public, ); }; };
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */ = {isa = PBXBuildFile; fileRef = 53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */; };
53B0360B184E5C860079FB77 /* SQRLHTTPServer.m in Sources */ = {isa = PBXBuildFile; fileRef = 53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */; };
53B70030181E9460000FFBD0 /* OHHTTPStubs.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */; };
53C3186F1816C38A0074B481 /* SQRLDownloadManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C318691816C38A0074B481 /* SQRLDownloadManager.h */; };
53C318701816C38A0074B481 /* SQRLDownloadManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */; };
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C3186B1816C38A0074B481 /* SQRLURLConnection.h */; };
53C318721816C38A0074B481 /* SQRLURLConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186C1816C38A0074B481 /* SQRLURLConnection.m */; };
53C318731816C38A0074B481 /* SQRLResumableDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = 53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */; };
53C318741816C38A0074B481 /* SQRLResumableDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */; };
53C3187C1816CCD70074B481 /* SQRLDownloadManagerSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */; };
53C3187D1816CCD70074B481 /* SQRLURLConnectionSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */; };
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */ = {isa = PBXBuildFile; fileRef = D000218D17BACEFF0050109A /* SQRLZipArchiver.m */; };
D000219717BAD34D0050109A /* TestApplication.app.zip in Resources */ = {isa = PBXBuildFile; fileRef = D000219617BAD34D0050109A /* TestApplication.app.zip */; };
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */; };
@@ -131,34 +109,6 @@
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 */;
@@ -411,8 +361,6 @@
dstPath = "";
dstSubfolderSpec = 10;
files = (
53542FCE184F86F1003A6AFD /* ReactiveCocoa.framework in Copy Frameworks */,
53542FC0184F86EC003A6AFD /* Mantle.framework in Copy Frameworks */,
D08D4E4917B451F20012B22D /* Squirrel.framework in Copy Frameworks */,
);
name = "Copy Frameworks";
@@ -432,22 +380,11 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RACSignal+SQRLFileManagerExtensions.h"; sourceTree = "<group>"; };
5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RACSignal+SQRLFileManagerExtensions.m"; sourceTree = "<group>"; };
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SQRLDownload+Private.h"; sourceTree = "<group>"; };
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+SQRLExtensions.h"; sourceTree = "<group>"; };
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+SQRLExtensions.m"; sourceTree = "<group>"; };
53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSHTTPURLResponse+SQRLExtensions.h"; sourceTree = "<group>"; };
53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSHTTPURLResponse+SQRLExtensions.m"; sourceTree = "<group>"; };
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSHTTPURLResponseExtensionsSpec.m; sourceTree = "<group>"; };
534FF36017D8E90A0020A51A /* com.github.Squirrel.TestApplication.TestService.xpc */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = com.github.Squirrel.TestApplication.TestService.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
534FF36417D8E90A0020A51A /* TestService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "TestService-Info.plist"; sourceTree = "<group>"; };
534FF36617D8E90A0020A51A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
534FF36817D8E90A0020A51A /* TestService-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "TestService-Prefix.pch"; sourceTree = "<group>"; };
534FF36917D8E90A0020A51A /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadedUpdateSpec.m; sourceTree = "<group>"; };
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLDownload.h; sourceTree = "<group>"; };
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownload.m; sourceTree = "<group>"; };
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDeepCodesignSpec.m; sourceTree = "<group>"; };
53710D7D17D8F5C300A992DE /* bootstrap */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = bootstrap; sourceTree = "<group>"; };
53710D7E17D8F5C300A992DE /* cibuild */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = cibuild; sourceTree = "<group>"; };
@@ -457,19 +394,10 @@
53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = "<group>"; };
53710D8317D8F5C300A992DE /* xcodebuild.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = xcodebuild.awk; sourceTree = "<group>"; };
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Squirrel-Constants.h"; sourceTree = "<group>"; };
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Squirrel-Constants.m"; sourceTree = "<group>"; };
53AACF4917E9CA0500B41027 /* SQRLUpdate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLUpdate.h; sourceTree = "<group>"; };
53AACF4A17E9CA0500B41027 /* SQRLUpdate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdate.m; sourceTree = "<group>"; };
53B03609184E5C860079FB77 /* SQRLHTTPServer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLHTTPServer.h; sourceTree = "<group>"; };
53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLHTTPServer.m; sourceTree = "<group>"; };
53C318691816C38A0074B481 /* SQRLDownloadManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLDownloadManager.h; sourceTree = "<group>"; };
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadManager.m; sourceTree = "<group>"; };
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLURLConnection.h; sourceTree = "<group>"; };
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLURLConnection.m; sourceTree = "<group>"; };
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLResumableDownload.h; sourceTree = "<group>"; };
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLResumableDownload.m; sourceTree = "<group>"; };
53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLDownloadManagerSpec.m; sourceTree = "<group>"; };
53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLURLConnectionSpec.m; sourceTree = "<group>"; };
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OHHTTPStubs.xcodeproj; path = External/ReactiveCocoa/external/specta/../../../OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj; sourceTree = "<group>"; };
D000218C17BACEFF0050109A /* SQRLZipArchiver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLZipArchiver.h; sourceTree = "<group>"; };
D000218D17BACEFF0050109A /* SQRLZipArchiver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLZipArchiver.m; sourceTree = "<group>"; };
D000219617BAD34D0050109A /* TestApplication.app.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = TestApplication.app.zip; sourceTree = "<group>"; };
@@ -593,7 +521,6 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
53542FCF184F870C003A6AFD /* Mantle.framework in Frameworks */,
D0F3F06417E83B0B003AFA6B /* ReactiveCocoa.framework in Frameworks */,
D08D4E4717B451E50012B22D /* Squirrel.framework in Frameworks */,
D08D4E0D17B451500012B22D /* Cocoa.framework in Frameworks */,
@@ -618,7 +545,6 @@
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 */,
@@ -670,30 +596,13 @@
path = script;
sourceTree = "<group>";
};
53B70015181E944F000FFBD0 /* Products */ = {
53A603F7182134D3002DB2C7 /* Supporting Files */ = {
isa = PBXGroup;
children = (
53B70021181E944F000FFBD0 /* libOHHTTPStubs-iOS.a */,
53B70023181E944F000FFBD0 /* UnitTests.octest */,
53B70025181E944F000FFBD0 /* OHHTTPStubs.framework */,
53A60406182134F9002DB2C7 /* Squirrel-Constants.h */,
53A60407182134F9002DB2C7 /* Squirrel-Constants.m */,
);
name = Products;
sourceTree = "<group>";
};
53C3185A1816C3730074B481 /* Downloads */ = {
isa = PBXGroup;
children = (
53C318691816C38A0074B481 /* SQRLDownloadManager.h */,
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */,
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */,
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */,
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */,
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */,
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */,
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */,
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */,
);
name = Downloads;
name = "Supporting Files";
sourceTree = "<group>";
};
D00F5B8217E82CFB009A4818 /* Extensions */ = {
@@ -701,8 +610,6 @@
children = (
D00F5B8917E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h */,
D00F5B8A17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m */,
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */,
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */,
);
name = Extensions;
sourceTree = "<group>";
@@ -737,10 +644,6 @@
D0964B3B17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m */,
D0EDBE9A17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.h */,
D0EDBE9B17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m */,
53390934184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h */,
53390935184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m */,
5316AF78188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h */,
5316AF79188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m */,
);
name = Extensions;
sourceTree = "<group>";
@@ -763,8 +666,7 @@
534FF36217D8E90A0020A51A /* TestService */,
D08D4E0F17B451500012B22D /* Supporting Files */,
);
name = TestApplication;
path = ../TestApplication;
path = TestApplication;
sourceTree = "<group>";
};
D08D4E0F17B451500012B22D /* Supporting Files */ = {
@@ -851,6 +753,7 @@
D0EDBE9F17B0C6930058BC3C /* Shared */,
D0C22BE0179CC00E00158214 /* Squirrel */,
D0C22BF6179CC00E00158214 /* SquirrelTests */,
D08D4E0E17B451500012B22D /* TestApplication */,
D014AC0317B97885007D79D0 /* ShipIt */,
D0C22BD9179CC00E00158214 /* Frameworks */,
53710D7C17D8F5C300A992DE /* Scripts */,
@@ -877,7 +780,6 @@
children = (
D097B9BC17BB4777006C3FEB /* IOKit.framework */,
D014ABB917B97403007D79D0 /* ServiceManagement.framework */,
53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */,
D0F3F01417E83918003AFA6B /* Expecta.xcodeproj */,
D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */,
D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */,
@@ -918,7 +820,6 @@
D0C22BF6179CC00E00158214 /* SquirrelTests */ = {
isa = PBXGroup;
children = (
D08D4E0E17B451500012B22D /* TestApplication */,
D0C22C4E179CC18000158214 /* Specs */,
D0C22BF7179CC00E00158214 /* Supporting Files */,
);
@@ -930,8 +831,6 @@
children = (
D049059E180558B7004E683E /* SQRLTestUpdate.h */,
D049059F180558B7004E683E /* SQRLTestUpdate.m */,
53B03609184E5C860079FB77 /* SQRLHTTPServer.h */,
53B0360A184E5C860079FB77 /* SQRLHTTPServer.m */,
D000219617BAD34D0050109A /* TestApplication.app.zip */,
D09D244917B59AB30001FAF8 /* TestApplication 2.1.app */,
D0C22BF8179CC00E00158214 /* SquirrelTests-Info.plist */,
@@ -1009,7 +908,6 @@
isa = PBXGroup;
children = (
D0964B2817F2DEE700D88BF7 /* Updates */,
53C3185A1816C3730074B481 /* Downloads */,
D0964B3417F2DEF500D88BF7 /* Archives */,
D0964B3517F2DF3100D88BF7 /* IPC */,
);
@@ -1020,7 +918,6 @@
isa = PBXGroup;
children = (
D00F5B8D17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m */,
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */,
D0EDBEE017B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m */,
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */,
D049059A180554BA004E683E /* SQRLDirectoryManagerSpec.m */,
@@ -1032,9 +929,6 @@
D0C22C4F179CC18C00158214 /* SQRLUpdaterSpec.m */,
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */,
D000219817BAD35C0050109A /* SQRLZipArchiverSpec.m */,
53C318791816CCD70074B481 /* SQRLDownloadManagerSpec.m */,
53C3187A1816CCD70074B481 /* SQRLURLConnectionSpec.m */,
53542FD1184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m */,
);
name = Specs;
sourceTree = "<group>";
@@ -1046,6 +940,7 @@
D0A56BF61804BABB00A84EDC /* Code Signing */,
D0A56BE71804BA2400A84EDC /* State */,
D06B58BC18032BE800656D97 /* Transactions */,
53A603F7182134D3002DB2C7 /* Supporting Files */,
);
name = Shared;
path = Squirrel;
@@ -1094,19 +989,12 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */,
D0964B3C17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.h in Headers */,
D0C22C4C179CC15100158214 /* SQRLUpdater.h in Headers */,
5316AFAC188F0A8E00C070DF /* NSData+SQRLExtensions.h in Headers */,
D0C22C52179CC23900158214 /* Squirrel.h in Headers */,
5316AFA8188F002100C070DF /* SQRLDownload+Private.h in Headers */,
53390936184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.h in Headers */,
D0964B3817F2E01500D88BF7 /* SQRLDownloadedUpdate.h in Headers */,
53AACF4B17E9CA0500B41027 /* SQRLUpdate.h in Headers */,
5316AF7A188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.h in Headers */,
53C318731816C38A0074B481 /* SQRLResumableDownload.h in Headers */,
536CD4E2183E5D7F000C0FD1 /* SQRLDownload.h in Headers */,
53C3186F1816C38A0074B481 /* SQRLDownloadManager.h in Headers */,
53A60408182134F9002DB2C7 /* Squirrel-Constants.h in Headers */,
D00F5B8B17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1201,11 +1089,11 @@
D0C22BEA179CC00E00158214 /* Sources */,
D0C22BEB179CC00E00158214 /* Frameworks */,
D0C22BEC179CC00E00158214 /* Resources */,
D0C22BED179CC00E00158214 /* ShellScript */,
);
buildRules = (
);
dependencies = (
53B7002F181E9456000FFBD0 /* PBXTargetDependency */,
D0F3F03317E83924003AFA6B /* PBXTargetDependency */,
D0F3F03517E83924003AFA6B /* PBXTargetDependency */,
D08D4E4C17B4520C0012B22D /* PBXTargetDependency */,
@@ -1246,10 +1134,6 @@
ProductGroup = D0A56B971804B1FD00A84EDC /* Products */;
ProjectRef = D0A56B961804B1FD00A84EDC /* Mantle.xcodeproj */;
},
{
ProductGroup = 53B70015181E944F000FFBD0 /* Products */;
ProjectRef = 53E1E674181E942300ED813C /* OHHTTPStubs.xcodeproj */;
},
{
ProductGroup = D0F3F04B17E83A1D003AFA6B /* Products */;
ProjectRef = D0F3F04A17E83A1D003AFA6B /* ReactiveCocoa.xcodeproj */;
@@ -1271,27 +1155,6 @@
/* 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;
@@ -1477,6 +1340,22 @@
};
/* 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;
@@ -1494,9 +1373,9 @@
D014AC1117B9789C007D79D0 /* SQRLInstaller.m in Sources */,
D043F77217FF40C90012F996 /* SQRLTerminationListener.m in Sources */,
D014AC0517B97885007D79D0 /* ShipIt-main.m in Sources */,
5316AF95188EFFA100C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */,
D014AC1917B979A8007D79D0 /* SQRLCodeSignature.m in Sources */,
D06B58B618032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
53A6040A182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
D014AC1A17B979AA007D79D0 /* NSError+SQRLVerbosityExtensions.m in Sources */,
D0D2B6271804E903000EA901 /* SQRLDirectoryManager.m in Sources */,
D0964B3E17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m in Sources */,
@@ -1517,25 +1396,19 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5316AFAD188F0A8E00C070DF /* NSData+SQRLExtensions.m in Sources */,
D0964B3D17F2E20B00D88BF7 /* NSBundle+SQRLVersionExtensions.m in Sources */,
F6F2ED8417A33C4900F4D220 /* SQRLCodeSignature.m in Sources */,
D0D2B6261804E903000EA901 /* SQRLDirectoryManager.m in Sources */,
53C318741816C38A0074B481 /* SQRLResumableDownload.m in Sources */,
5316AF7B188EFC1800C070DF /* RACSignal+SQRLFileManagerExtensions.m in Sources */,
D0964B3917F2E01500D88BF7 /* SQRLDownloadedUpdate.m in Sources */,
D014ABB817B96CB3007D79D0 /* SQRLShipItLauncher.m in Sources */,
53C318721816C38A0074B481 /* SQRLURLConnection.m in Sources */,
D000218F17BACEFF0050109A /* SQRLZipArchiver.m in Sources */,
D0C22C4D179CC15100158214 /* SQRLUpdater.m in Sources */,
D0EDBE9D17B0C68E0058BC3C /* NSError+SQRLVerbosityExtensions.m in Sources */,
53AACF4C17E9CA0500B41027 /* SQRLUpdate.m in Sources */,
D0A56BEB1804BA3C00A84EDC /* SQRLShipItState.m in Sources */,
53C318701816C38A0074B481 /* SQRLDownloadManager.m in Sources */,
53A60409182134F9002DB2C7 /* Squirrel-Constants.m in Sources */,
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */,
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1545,20 +1418,15 @@
files = (
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */,
D000219917BAD35C0050109A /* SQRLZipArchiverSpec.m in Sources */,
53542FD2184F8736003A6AFD /* SQRLDownloadedUpdateSpec.m in Sources */,
D043F77E17FF43640012F996 /* SQRLTerminationListener.m in Sources */,
D09D244117B595470001FAF8 /* SQRLInstallerSpec.m in Sources */,
D0C22C50179CC18C00158214 /* SQRLUpdaterSpec.m in Sources */,
D0EDBEDD17B0DB3E0058BC3C /* SQRLTestCase.m in Sources */,
5339094A184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m in Sources */,
53C3187C1816CCD70074B481 /* SQRLDownloadManagerSpec.m in Sources */,
D06B58D118035B5A00656D97 /* SQRLTerminationListenerSpec.m in Sources */,
D0EDBEE117B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m in Sources */,
53710D7417D8F59700A992DE /* SQRLDeepCodesignSpec.m in Sources */,
D00F5B8E17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m in Sources */,
53B0360B184E5C860079FB77 /* SQRLHTTPServer.m in Sources */,
D049059B180554BA004E683E /* SQRLDirectoryManagerSpec.m in Sources */,
53C3187D1816CCD70074B481 /* SQRLURLConnectionSpec.m in Sources */,
D061E40217FD580D00CC134A /* SQRLInstaller.m in Sources */,
D04905A0180558B7004E683E /* SQRLTestUpdate.m in Sources */,
D049059D18055671004E683E /* SQRLShipItStateSpec.m in Sources */,
@@ -1573,11 +1441,6 @@
target = 534FF35F17D8E90A0020A51A /* TestService */;
targetProxy = 534FF37017D8E9170020A51A /* PBXContainerItemProxy */;
};
53B7002F181E9456000FFBD0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = OHHTTPStubs;
targetProxy = 53B7002E181E9456000FFBD0 /* PBXContainerItemProxy */;
};
D014AC1717B9793A007D79D0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D014AC0017B97885007D79D0 /* ShipIt */;
@@ -20,20 +20,6 @@
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
-19
View File
@@ -1,19 +0,0 @@
//
// NSData+SQRLExtensions.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSData (SQRLExtensions)
// Returns an NSData for the SHA1 hash of the receiver.
- (NSData *)sqrl_SHA1Hash;
// Returns the base16 encoding of the receiver.
- (NSString *)sqrl_base16String;
@end
-31
View File
@@ -1,31 +0,0 @@
//
// NSData+SQRLExtensions.m
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "NSData+SQRLExtensions.h"
#import <CommonCrypto/CommonCrypto.h>
@implementation NSData (SQRLExtensions)
- (NSData *)sqrl_SHA1Hash {
unsigned char hash[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(self.bytes, (CC_LONG)self.length, hash);
return [NSData dataWithBytes:hash length:sizeof(hash) / sizeof(*hash)];
}
- (NSString *)sqrl_base16String {
NSMutableString *base16 = [NSMutableString stringWithCapacity:self.length * 2];
uint8_t *bytes = (uint8_t *)self.bytes;
for (NSUInteger idx = 0; idx < self.length; idx++) {
uint8_t byte = *(bytes + idx);
[base16 appendFormat:@"%02hhx", byte];
}
return base16;
}
@end
@@ -1,18 +0,0 @@
//
// NSHTTPURLResponse+SQRLExtensions.h
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSHTTPURLResponse (SQRLExtensions)
// Case insensitively look up a header field value.
//
// Returns the value of the header if present or nil otherwise.
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field;
@end
@@ -1,26 +0,0 @@
//
// NSHTTPURLResponse+SQRLExtensions.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "NSHTTPURLResponse+SQRLExtensions.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@implementation NSHTTPURLResponse (SQRLExtensions)
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field {
return [[[self.allHeaderFields.rac_signal
filter:^ BOOL (RACTuple *keyValuePair) {
return [keyValuePair[0] caseInsensitiveCompare:field] == NSOrderedSame;
}]
reduceEach:^(NSString *key, NSString *value) {
return value;
}]
first];
}
@end
@@ -1,15 +0,0 @@
//
// RACSignal+SQRLFileManagerExtensions.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface RACSignal (SQRLFileManagerExtensions)
- (RACSignal *)sqrl_tryCreateDirectory;
@end
@@ -1,21 +0,0 @@
//
// RACSignal+SQRLFileManagerExtensions.m
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "RACSignal+SQRLFileManagerExtensions.h"
@implementation RACSignal (SQRLFileManagerExtensions)
- (RACSignal *)sqrl_tryCreateDirectory {
return [[self
try:^(NSURL *directoryURL, NSError **errorRef) {
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:nil error:errorRef];
}]
setNameWithFormat:@"%@ -sqrl_tryCreateDirectory", self];
}
@end
+16 -13
View File
@@ -97,12 +97,15 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
[subscriber.disposable addDisposable:SQRLCreateTransaction(name, description)];
[self subscribe:subscriber];
}]
setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
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];
}
- (RACSignal *)sqrl_addSubscriptionTransactionWithName:(NSString *)name description:(NSString *)descriptionFormat, ... {
@@ -114,13 +117,13 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
[self subscribe:subscriber];
[transactionDisposable dispose];
}]
setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
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];
}
@end
-10
View File
@@ -9,16 +9,6 @@
#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.
+7 -12
View File
@@ -10,11 +10,7 @@
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
const NSInteger SQRLCodeSignatureErrorDidNotPass = -1;
const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
#import "Squirrel-Constants.h"
@interface SQRLCodeSignature ()
@@ -83,7 +79,7 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
return [[RACSignal create:^(id<RACSubscriber> subscriber) {
return [[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
SecStaticCodeRef staticCode = NULL;
OSStatus result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
@@ -99,11 +95,9 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
NSString *failureReason = CFBridgingRelease(SecCopyErrorMessageString(result, NULL));
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return;
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return nil;
}
if (subscriber.disposable.disposed) return;
CFErrorRef validityError = NULL;
result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, (__bridge SecRequirementRef)self.requirement, &validityError);
@@ -120,11 +114,12 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return;
[subscriber sendError:[NSError errorWithDomain:SQRLErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return nil;
}
[subscriber sendCompleted];
return nil;
}] setNameWithFormat:@"-verifyCodeSignatureOfBundle: %@", bundleURL];
}
+3 -16
View File
@@ -26,27 +26,14 @@
// on-disk locations for. This must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
// Finds or creates an Application Support subdirectory for the receiver’s
// application identifier.
// Finds or creates an Application Support folder.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
@property (readonly, nonatomic) RACSignal *applicationSupportURL;
// Finds or creates a downloads directory to store update downloads in prior to
// installation. This is an applicationSupportURL subdirectory.
//
// Returns a signal which sends a URL then completes, or errors.
@property (readonly, nonatomic) RACSignal *downloadDirectoryURL;
// Creates a new directory to unpack a downloaded update into prior to
// installation. This is an applicationSupportURL subdirectory.
//
// Returns a signal which sends a URL then completes, or errors.
- (RACSignal *)createUniqueUpdateDirectoryURL;
- (RACSignal *)applicationSupportURL;
// Determines where archived `SQRLShipItState` should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
@property (readonly, nonatomic) RACSignal *shipItStateURL;
- (RACSignal *)shipItStateURL;
@end
+8 -50
View File
@@ -8,8 +8,6 @@
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import "RACSignal+SQRLFileManagerExtensions.h"
@interface SQRLDirectoryManager ()
@@ -55,65 +53,25 @@
#pragma mark Folder URLs
+ (NSString *)fileSystemNameForIdentifier:(NSString *)identifier {
// Periods are problematic in the filesystem because they denote file type.
// A directory can become a package which can be undesirable.
return [identifier stringByReplacingOccurrencesOfString:@"." withString:@"~"];
}
- (RACSignal *)applicationSupportURL {
return [[[[RACSignal
return [[RACSignal
defer:^{
NSError *error = nil;
NSURL *directoryURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (directoryURL == nil) {
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (folderURL == nil) {
return [RACSignal error:error];
}
return [RACSignal return:directoryURL];
}]
map:^ (NSURL *directoryURL) {
RACSignal *identifierComponents = [@[ self.applicationIdentifier, @"com.github.Squirrel" ].rac_signal
map:^(NSString *name) {
return [self.class fileSystemNameForIdentifier:name];
}];
folderURL = [folderURL URLByAppendingPathComponent:self.applicationIdentifier];
if (![NSFileManager.defaultManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:&error]) {
return [RACSignal error:error];
}
RACSignal *pathComponents = [directoryURL.pathComponents.rac_signal concat:identifierComponents];
return [NSURL fileURLWithPathComponents:[pathComponents array]];
return [RACSignal return:folderURL];
}]
sqrl_tryCreateDirectory]
setNameWithFormat:@"%@ -applicationSupportURL", self];
}
- (RACSignal *)downloadDirectoryURL {
return [[[[self
applicationSupportURL]
map:^(NSURL *directoryURL) {
return [directoryURL URLByAppendingPathComponent:@"downloads"];
}]
sqrl_tryCreateDirectory]
setNameWithFormat:@"%@ -downloadDirectoryURL", self];
}
- (RACSignal *)createUniqueUpdateDirectoryURL {
return [[[[self
applicationSupportURL]
map:^(NSURL *directoryURL) {
// noindex so that Spotlight doesn't pick up apps pending update and
// add them to the Launch Services database.
return [[directoryURL URLByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString] URLByAppendingPathExtension:@"noindex"];
}]
try:^(NSURL *directoryURL, NSError **errorRef) {
// Explicitly just provide the owner with permission, discarding the
// current umask. This matches the `mkdtemp` behaviour.
NSDictionary *directoryAttributes = @{
NSFilePosixPermissions: @(S_IRWXU),
};
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:directoryAttributes error:errorRef];
}]
setNameWithFormat:@"%@ -createUniqueUpdateDirectoryURL", self];
}
- (RACSignal *)shipItStateURL {
return [[[self
applicationSupportURL]
-21
View File
@@ -1,21 +0,0 @@
//
// SQRLDownload+Private.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLDownload.h"
@interface SQRLDownload ()
// Designated initialiser.
//
// request - Must not be nil. The request this download is for.
// fileURL - Must not be nil. The file location to save received data to.
//
// Returns an initialised download suitable for saving response data.
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL;
@end
-31
View File
@@ -1,31 +0,0 @@
//
// SQRLDownload.h
// Squirrel
//
// Created by Keith Duncan on 21/11/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class RACSignal;
// A download is a file which holds the response body for a request and can be
// resumed by a subsequent request if the initial transfer is interrupted.
//
// Don't create a download directly, instead retrieve one from
// `SQRLDownloadManager`.
@interface SQRLDownload : MTLModel
// The `request` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSURLRequest *request;
// The `fileURL` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSURL *fileURL;
// Calculate a request suitable for resuming the download.
//
// Returns a signal which sends an `NSURLRequest` then completes.
@property (readonly, nonatomic) RACSignal *resumableRequest;
@end
-29
View File
@@ -1,29 +0,0 @@
//
// SQRLDownload.m
// Squirrel
//
// Created by Keith Duncan on 21/11/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownload.h"
#import "SQRLDownload+Private.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@implementation SQRLDownload
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL {
return [self initWithDictionary:@{
@keypath(self.request): request,
@keypath(self.fileURL): fileURL,
} error:NULL];
}
- (RACSignal *)resumableRequest {
return [[RACSignal
return:self.request]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
-61
View File
@@ -1,61 +0,0 @@
//
// SQRLDownloadController.h
// Squirrel
//
// Created by Keith Duncan on 26/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class SQRLDownload;
@class SQRLResumableDownload;
@class RACSignal;
@class SQRLDirectoryManager;
// Stores metadata for download resumption, and manages the disk locations for
// where they're stored.
@interface SQRLDownloadManager : NSObject
// Default download manager, initializes a download manager with the current
// application directory manager. Stores downloads in
// `directoryManager.downloadDirectoryURL`.
+ (instancetype)defaultDownloadManager;
// Designated initialiser.
//
// directoryManager - Must not be nil.
//
// Returns a download manager which can resume previously started downloads for
// the initialised directory manager's application.
- (instancetype)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
// Clean the resumable download state, removes downloaded data and tracking
// state.
//
// Returns a signal which errors or completes when all file operations have been
// attempted.
- (RACSignal *)removeAllResumableDownloads;
// Retrieve a previously started download, or initialise a new download.
//
// Previously started downloads may have been removed and require
// reinitialising.
//
// request - The request you are performing and require a disk location to save
// the response body to. Must not be nil.
//
// Returns a signal which sends an `SQRLDownload` object then completes, or
// errors.
- (RACSignal *)downloadForRequest:(NSURLRequest *)request;
// Store metadata for a download so that it can be resumed later.
//
// download - The download being stored for future resumption. Must not be nil.
// request - The request for which the download can be resumed. Must not be
// nil.
//
// Returns a signal which completes or errors.
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request;
@end
-239
View File
@@ -1,239 +0,0 @@
//
// SQRLDownloadController.m
// Squirrel
//
// Created by Keith Duncan on 26/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownloadManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SQRLDirectoryManager.h"
#import "SQRLDownload.h"
#import "SQRLDownload+Private.h"
#import "SQRLResumableDownload.h"
#import "NSData+SQRLExtensions.h"
@interface SQRLDownloadManager ()
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
@property (nonatomic, assign, readonly) dispatch_queue_t queue;
// Reads the download index.
//
// Returns a signal which sends the download index at time of read then
// completes, or errors.
- (RACSignal *)readDownloadIndex;
// Write a new download index.
//
// block - Map from the old download index to the new download index. Writing
// is serialised, subsequent writers will receive the output of the
// previous map block. If the output of the map is equal to the input
// no write is attempted and the returned signal completes.
//
// Returns a signal which completes or errors.
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block;
@end
@implementation SQRLDownloadManager
+ (instancetype)defaultDownloadManager {
static SQRLDownloadManager *defaultDownloadManager;
static dispatch_once_t defaultDownloadManagerPredicate;
dispatch_once(&defaultDownloadManagerPredicate, ^{
defaultDownloadManager = [[self alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
});
return defaultDownloadManager;
}
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
NSParameterAssert(directoryManager != nil);
self = [super init];
if (self == nil) return nil;
_directoryManager = directoryManager;
_queue = dispatch_queue_create("com.github.Squirrel.SQRLDownloadManager.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
return self;
}
- (void)dealloc {
if (_queue != NULL) dispatch_release(_queue);
}
- (RACSignal *)removeAllResumableDownloads {
return [[[RACSignal
merge:@[
[self downloadStoreIndexFileLocation],
[self.directoryManager downloadDirectoryURL]
]]
flattenMap:^ (NSURL *location) {
return [[RACSignal
defer:^{
NSError *error;
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:location error:&error];
return (remove ? [RACSignal empty] : [RACSignal error:error]);
}]
catch:^(NSError *error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
return [RACSignal error:error];
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)downloadStoreIndexFileLocation {
return [[[self.directoryManager
applicationSupportURL]
map:^(NSURL *directory) {
return [directory URLByAppendingPathComponent:@"DownloadIndex.plist"];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)readDownloadIndex {
return [[[self
downloadStoreIndexFileLocation]
flattenMap:^(NSURL *location) {
return [RACSignal
create:^(id<RACSubscriber> subscriber) {
dispatch_async(self.queue, ^{
if (subscriber.disposable.disposed) return;
NSError *error;
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
if (propertyList == nil) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:propertyList];
[subscriber sendCompleted];
});
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block {
NSParameterAssert(block != nil);
return [[[self
downloadStoreIndexFileLocation]
flattenMap:^(NSURL *location) {
return [RACSignal create:^(id<RACSubscriber> subscriber) {
dispatch_barrier_async(self.queue, ^{
if (subscriber.disposable.disposed) return;
NSError *error;
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
if (propertyList == nil) {
[subscriber sendError:error];
return;
}
NSDictionary *newPropertyList = block([propertyList mutableCopy]);
if ([newPropertyList isEqual:propertyList]) {
[subscriber sendCompleted];
return;
}
NSData *newData = [NSKeyedArchiver archivedDataWithRootObject:newPropertyList];
if (newData == nil) {
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
[subscriber sendError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListWriteStreamError userInfo:errorInfo]];
return;
}
BOOL write = [newData writeToURL:location options:NSDataWritingAtomic error:&error];
if (!write) {
[subscriber sendError:error];
return;
}
[subscriber sendCompleted];
});
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (NSDictionary *)readPropertyListWithContentsOfURL:(NSURL *)location error:(NSError **)errorRef {
NSData *propertyListData = [NSData dataWithContentsOfURL:location options:NSDataReadingUncached error:NULL];
if (propertyListData == nil) {
return @{};
}
NSDictionary *propertyList = [NSKeyedUnarchiver unarchiveObjectWithData:propertyListData];
if (propertyList == nil) {
if (errorRef != NULL) {
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
*errorRef = [NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListReadCorruptError userInfo:errorInfo];
}
return nil;
}
return propertyList;
}
+ (NSString *)keyForURL:(NSURL *)URL {
return URL.absoluteString;
}
+ (NSString *)fileNameForURL:(NSURL *)URL {
NSString *key = [self keyForURL:URL];
return [[[key dataUsingEncoding:NSUTF8StringEncoding] sqrl_SHA1Hash] sqrl_base16String];
}
- (RACSignal *)downloadForRequest:(NSURLRequest *)request {
NSParameterAssert(request != nil);
return [[[[self
readDownloadIndex]
map:^(NSDictionary *index) {
NSString *key = [self.class keyForURL:request.URL];
return index[key];
}]
flattenMap:^(SQRLResumableDownload *download) {
if (download != nil) return [RACSignal return:download];
return [[[self.directoryManager
downloadDirectoryURL]
map:^(NSURL *downloadDirectory) {
return [downloadDirectory URLByAppendingPathComponent:[self.class fileNameForURL:request.URL]];
}]
map:^(NSURL *location) {
return [[SQRLDownload alloc] initWithRequest:request fileURL:location];
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request {
NSParameterAssert(download.response != nil);
NSParameterAssert(request != nil);
return [[self
writeDownloadIndexWithBlock:^(NSMutableDictionary *index) {
NSString *key = [self.class keyForURL:request.URL];
if (download != nil) {
index[key] = download;
} else {
[index removeObjectForKey:key];
}
return index;
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+1 -1
View File
@@ -11,7 +11,7 @@
@class SQRLUpdate;
// A SQRLUpdate that has been successfully downloaded to disk.
@interface SQRLDownloadedUpdate : MTLModel <MTLJSONSerializing>
@interface SQRLDownloadedUpdate : MTLModel
// The application bundle representing the downloaded and unarchived update.
@property (nonatomic, strong, readonly) NSBundle *bundle;
+3 -10
View File
@@ -7,7 +7,6 @@
//
#import "SQRLDownloadedUpdate.h"
#import "SQRLUpdate.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLDownloadedUpdate ()
@@ -41,15 +40,9 @@
#pragma mark MTLJSONSerializing
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{};
}
+ (NSValueTransformer *)updateJSONTransformer {
return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:SQRLUpdate.class];
}
+ (NSValueTransformer *)bundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
}];
}
@end
+10 -14
View File
@@ -8,9 +8,6 @@
#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;
@@ -34,9 +31,7 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
// There was an error moving a bundle across volumes.
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
@class RACSignal;
@class RACAction;
@class SQRLShipItState;
@class RACCommand;
@class SQRLDirectoryManager;
// Performs the installation of an update, using the `SQRLShipItState` on disk,
@@ -46,20 +41,21 @@ extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
// terminated.
@interface SQRLInstaller : NSObject
// Attempts to install the update or resume an in-progress installation.
// When executed with a `SQRLShipItState`, attempts to install the update or
// resume an in-progress installation.
//
// Returns a signal which completes or errors on an unspecified scheduler when
// Each execution will complete or error on an unspecified scheduler when
// installation has completed or failed.
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state;
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
// Aborts an installation, and attempts to restore the old version of the
// application if necessary.
// When executed with a `SQRLShipItState`, aborts an installation, and attempts
// to restore the old version of the application if necessary.
//
// This must not be executed while `installUpdateWithState:` is executing.
// This must not be executed while `installUpdateCommand` is executing.
//
// Returns a signal while completes or errors on an unspecified scheduler once
// Each execution will complete or error on an unspecified scheduler once
// aborting/recovery has finished.
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state;
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
// Initializes an installer using the given directory manager to read and write the
// state of the installation.
+90 -50
View File
@@ -18,8 +18,7 @@
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/xattr.h>
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
#import "Squirrel-Constants.h"
const NSInteger SQRLInstallerErrorBackupFailed = -1;
const NSInteger SQRLInstallerErrorReplacingTarget = -2;
@@ -145,34 +144,44 @@ 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 {
@@ -201,6 +210,7 @@ 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);
@@ -225,7 +235,7 @@ typedef struct {
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
}
SEL selector = dispatchTable[tableIndex].selector;
@@ -252,12 +262,13 @@ typedef struct {
nextState = dispatchTable[tableIndex + 1].installerState;
}
return [[[step
return [[step
doCompleted:^{
NSLog(@"Completed state %i", (int)installerState);
}]
ignoreValues]
concat:[RACSignal return:@(nextState)]];
then:^{
return [RACSignal return:@(nextState)];
}];
}];
return [[self
@@ -354,9 +365,11 @@ typedef struct {
// failure. Try recovering it if it did.
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
// Recovery succeeded, but we still want to pass
// through the original error.
concat:[RACSignal error:error]];
then:^{
// Recovery succeeded, but we still want to pass
// through the original error.
return [RACSignal error:error];
}];
}];
}]
flatten]
@@ -374,14 +387,37 @@ typedef struct {
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
concat:[[self
deleteBackupAtURL:backupBundleURL]
catchTo:[RACSignal empty]]];
then:^{
return [[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 {
@@ -423,23 +459,26 @@ typedef struct {
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
NSParameterAssert(backupURL != nil);
return [[RACSignal
return [[[RACSignal
defer:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
if ([NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
return [RACSignal empty];
} else {
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) {
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) == 0) {
return [RACSignal empty];
} else {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
}
@@ -453,11 +492,10 @@ typedef struct {
catch:^(NSError *error) {
if (backupBundleURL == nil) return [RACSignal error:error];
return [[[RACSignal
defer:^{
return [[[[self
installItemAtURL:bundleURL fromURL:backupBundleURL]
initially:^{
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
return [self installItemAtURL:bundleURL fromURL:backupBundleURL];
}]
doCompleted:^{
NSLog(@"Restored backup bundle to %@", bundleURL);
@@ -478,7 +516,9 @@ typedef struct {
return [[[[self
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
concat:[RACSignal return:@YES]]
then:^{
return [RACSignal return:@YES];
}]
catch:^(NSError *error) {
BOOL directory;
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
@@ -544,7 +584,7 @@ typedef struct {
return YES;
}];
return [enumerator.rac_promise start];
return enumerator.rac_sequence.signal;
}]
flattenMap:^(NSURL *URL) {
const char *path = URL.path.fileSystemRepresentation;
@@ -578,7 +618,7 @@ typedef struct {
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
return [NSError errorWithDomain:SQRLErrorDomain code:SQRLInstallerErrorMissingInstallationData userInfo:userInfo];
}
- (NSError *)errorByAddingDescription:(NSString *)description code:(NSInteger)code toError:(NSError *)error {
@@ -587,7 +627,7 @@ typedef struct {
if (description != nil) userInfo[NSLocalizedDescriptionKey] = description;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [NSError errorWithDomain:SQRLInstallerErrorDomain code:code userInfo:userInfo];
return [NSError errorWithDomain:SQRLErrorDomain code:code userInfo:userInfo];
}
@end
-27
View File
@@ -1,27 +0,0 @@
//
// SQRLResumableDownload.h
// Squirrel
//
// Created by Keith Duncan on 30/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SQRLDownload.h"
// A download which has already been started and already has partial data.
@interface SQRLResumableDownload : SQRLDownload
// Designated initialiser.
//
// request - See `SQRLDownload`.
// response - HTTP response whose body is being downloaded to `fileURL`.
// Must not be nil.
// fileURL - See `SQRLDownload`.
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL;
// The `response` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSHTTPURLResponse *response;
@end
-66
View File
@@ -1,66 +0,0 @@
//
// SQRLResumableDownload.m
// Squirrel
//
// Created by Keith Duncan on 30/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLResumableDownload.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSHTTPURLResponse+SQRLExtensions.h"
@implementation SQRLResumableDownload
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL {
NSParameterAssert(request != nil);
NSParameterAssert(response != nil);
NSParameterAssert(fileURL != nil);
return [self initWithDictionary:@{
@keypath(self.request): request,
@keypath(self.response): response,
@keypath(self.fileURL): fileURL,
} error:NULL];
}
- (BOOL)isEqual:(SQRLResumableDownload *)object {
if (self == object) return YES;
if (![object isKindOfClass:self.class]) return NO;
if (!(self.response == nil && object.response == nil) && ![self responseEqual:object.response]) return NO;
if (![object.fileURL isEqual:self.fileURL]) return NO;
return YES;
}
- (BOOL)responseEqual:(NSHTTPURLResponse *)response {
if (![response.URL isEqual:self.response.URL]) return NO;
if (response.statusCode != self.response.statusCode) return NO;
if (![response.allHeaderFields isEqual:self.response.allHeaderFields]) return NO;
return YES;
}
- (RACSignal *)resumableRequest {
return [[[super
resumableRequest]
map:^ NSURLRequest * (NSURLRequest *request) {
NSHTTPURLResponse *response = self.response;
NSString *ETag = [response sqrl_valueForHTTPHeaderField:@"ETag"];
if (ETag == nil) return request;
NSNumber *alreadyDownloadedSize = nil;
BOOL getAlreadyDownloadedSize = [self.fileURL getResourceValue:&alreadyDownloadedSize forKey:NSURLFileSizeKey error:NULL];
if (!getAlreadyDownloadedSize) return request;
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:ETag forHTTPHeaderField:@"If-Range"];
[newRequest setValue:[NSString stringWithFormat:@"%llu-", alreadyDownloadedSize.unsignedLongLongValue] forHTTPHeaderField:@"Range"];
return newRequest;
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
-6
View File
@@ -10,12 +10,6 @@
@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
+10 -11
View File
@@ -13,10 +13,7 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
#import "Squirrel-Constants.h"
@implementation SQRLShipItLauncher
@@ -71,7 +68,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
createSignal:^(id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
@@ -115,9 +112,9 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
return [RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}]];
}];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -132,11 +129,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
#if DEBUG
NSLog(@"Could not remove previous ShipIt job: %@", cfError);
#endif
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job: %@", error);
if (cfError != NULL) {
CFRelease(cfError);
cfError = NULL;
}
}
+4 -16
View File
@@ -8,22 +8,6 @@
#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;
@@ -41,6 +25,9 @@ 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.
@@ -50,6 +37,7 @@ typedef enum : NSInteger {
SQRLInstallerStateBackingUp,
SQRLInstallerStateInstalling,
SQRLInstallerStateVerifyingInPlace,
SQRLInstallerStateRelaunching
} SQRLInstallerState;
@class RACSignal;
+4 -8
View File
@@ -8,14 +8,10 @@
#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
@@ -33,7 +29,7 @@ const NSInteger SQRLShipItStateErrorArchiving = 3;
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
};
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
*error = [NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
}
return NO;
@@ -78,7 +74,7 @@ const NSInteger SQRLShipItStateErrorArchiving = 3;
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
}
return [RACSignal return:state];
@@ -97,7 +93,7 @@ const NSInteger SQRLShipItStateErrorArchiving = 3;
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
}
return [RACSignal return:data];
+4 -4
View File
@@ -45,7 +45,7 @@
return [[[[RACSignal
defer:^{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
return apps.rac_signal;
return [apps.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
}]
filter:^(NSRunningApplication *application) {
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
@@ -60,7 +60,7 @@
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
createSignal:^(id<RACSubscriber> subscriber) {
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, processIdentifier, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_registration_handler(source, ^{
@@ -72,10 +72,10 @@
});
dispatch_resume(source);
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
return [RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
dispatch_release(source);
}]];
}];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
}
-49
View File
@@ -1,49 +0,0 @@
//
// SQRLURLConnection.h
// Squirrel
//
// Created by Keith Duncan on 25/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class SQRLDownloadManager;
@class RACSignal;
// Resumably download a remote resource.
//
// If the resource has previously been downloaded this operation may return the
// URL of an existing file, contingent on the server matching the ETag of the
// original response.
@interface SQRLURLConnection : NSObject
// This is the designated initialiser. The body of the response is streamed to
// disk. If the response includes an ETag the download can be resumed
// automatically by subsequent download operations initialised with a request of
// equal URL.
//
// request - Must not be nil, the request is issued with NSURLConnection.
- (instancetype)initWithRequest:(NSURLRequest *)request __attribute__((nonnull (1)));
// Starts accumulating the resource body in memory, cancels when the
// subscription is disposed.
//
// Returns a signal when sends a tuple of `NSURLResponse` and `NSData`, then
// completes or errors.
- (RACSignal *)retrieve;
// Starts downloading the resource body to disk, resuming from a previous
// download if possible, cancels when the subscription is disposed.
//
// downloadManager - Must be non nil, determines where the downloads will be
// stored and resumed from. For a previously started download
// to be resumed, an equivalent download manager should be
// provided.
//
// Returns a signal which sends a tuple of `NSURLResponse` and a file
// scheme `NSURL` where the resource has been stored on disk, then completes, or
// errors.
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager __attribute__((nonnull (1)));
@end
-257
View File
@@ -1,257 +0,0 @@
//
// SQRLURLConnection.m
// Squirrel
//
// Created by Keith Duncan on 25/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLURLConnection.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
@interface SQRLURLConnection ()
// The request the downloader was initialised with.
@property (nonatomic, copy, readonly) NSURLRequest *request;
// Listens for invocations of `selector` on the receiver.
//
// The first argument of `selector` must be an `NSURLConnection` object.
//
// Returns a signal which sends the _second_ argument of `selector`, or
// RACUnit if there is only one argument, then completes when the receiver
// deallocates.
- (RACSignal *)signalForDelegateSelector:(SEL)selector;
// Subscribes to the connection delegate selectors and starts a connection for
// `request`.
//
// request - The request to perform, must not be nil.
// mapData - For each response, map the data received for that response to a
// result.
//
// Returns a signal which sends a tuple of the last response, and the first
// value from the last `mapData`, then completes, or errors.
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData;
@end
@implementation SQRLURLConnection
- (instancetype)initWithRequest:(NSURLRequest *)request {
NSParameterAssert(request != nil);
self = [self init];
if (self == nil) return nil;
_request = [request copy];
return self;
}
- (RACSignal *)signalForDelegateSelector:(SEL)selector {
NSParameterAssert(selector != NULL);
return [[self
rac_signalForSelector:selector]
map:^(RACTuple *args) {
return args[1] ?: RACUnit.defaultUnit;
}];
}
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData {
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
// A signal that will error if the connection fails for any reason.
RACSignal *errors = [[self
signalForDelegateSelector:@selector(connection:didFailWithError:)]
flattenMap:^(NSError *error) {
return [RACSignal error:error];
}];
// Sends (or replays) RACUnit when the connection has finished
// loading successfully.
RACSignal *finished = [[[[self
signalForDelegateSelector:@selector(connectionDidFinishLoading:)]
take:1]
promiseOnScheduler:RACScheduler.immediateScheduler]
start];
// A signal of all `NSURLResponse`s received on the connection.
RACSignal *responses = [[self
signalForDelegateSelector:@selector(connection:didReceiveResponse:)]
takeUntil:finished];
// A signal of all `NSData` received on the connection.
RACSignal *data = [[self
signalForDelegateSelector:@selector(connection:didReceiveData:)]
takeUntil:finished];
[[[[[RACSignal
merge:@[ responses, errors ]]
takeUntil:finished]
map:^(NSURLResponse *response) {
return [RACSignal
zip:@[
[RACSignal return:response],
mapData(response, data),
]];
}]
switchToLatest]
subscribe:subscriber];
NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
delegateQueue.maxConcurrentOperationCount = 1;
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
connection.delegateQueue = delegateQueue;
[connection start];
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
[connection cancel];
}]];
}]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), request];
}
#pragma mark Download
- (RACSignal *)truncateDownload:(SQRLDownload *)download {
NSParameterAssert(download != nil);
return [[[[RACSignal
defer:^{
NSError *error = nil;
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:download.fileURL error:&error];
return (remove ? [RACSignal empty] : [RACSignal error:error]);
}]
catch:^(NSError *error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
return [RACSignal error:error];
}]
concat:[RACSignal return:download]]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
}
- (RACSignal *)recordDownload:(SQRLResumableDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager {
NSParameterAssert(download != nil);
NSParameterAssert(downloadManager != nil);
return [[[downloadManager
setDownload:download forRequest:self.request]
concat:[RACSignal return:download]]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
}
- (RACSignal *)prepareDownload:(SQRLDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager forResponse:(NSURLResponse *)response {
return [[RACSignal
defer:^{
if (![response isKindOfClass:NSHTTPURLResponse.class]) {
return [self truncateDownload:download];
}
NSHTTPURLResponse *httpResponse = (id)response;
RACSignal *downloadSignal;
if (httpResponse.statusCode != 206 /* Partial Data */) {
downloadSignal = [self truncateDownload:download];
} else {
downloadSignal = [RACSignal empty];
}
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:self.request response:httpResponse fileURL:download.fileURL];
return [[downloadSignal
ignoreValues]
concat:[self recordDownload:newDownload downloadManager:downloadManager]];
}]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), response];
}
- (BOOL)appendData:(NSData *)data toURL:(NSURL *)fileURL error:(NSError **)errorRef {
NSOutputStream *outputStream = [NSOutputStream outputStreamWithURL:fileURL append:YES];
[outputStream open];
@onExit {
[outputStream close];
};
uint8_t const *bytes = data.bytes;
size_t length = data.length;
while (length > 0) {
NSInteger written = [outputStream write:bytes maxLength:length];
if (written == -1) {
NSError *streamError = outputStream.streamError;
if ([streamError.domain isEqualToString:NSPOSIXErrorDomain] && streamError.code == EINTR) continue;
if (errorRef != NULL) *errorRef = streamError;
return NO;
}
bytes += written;
length -= written;
}
return YES;
}
#pragma mark Start
- (RACSignal *)retrieve {
return [[self
connectionSignalWithRequest:self.request mapData:^(id _, RACSignal *data) {
return [data
aggregateWithStart:[NSMutableData data] reduce:^(NSMutableData *running, NSData *data) {
[running appendData:data];
return running;
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager {
NSParameterAssert(downloadManager != nil);
RACSignal *download = [[[downloadManager
downloadForRequest:self.request]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred];
return [[[download
flattenMap:^(SQRLDownload *download) {
return [download resumableRequest];
}]
flattenMap:^(NSURLRequest *request) {
return [self connectionSignalWithRequest:request mapData:^(NSURLResponse *response, RACSignal *data) {
RACSignal *downloadURL = [[[[download
flattenMap:^(SQRLDownload *download) {
return [self prepareDownload:download downloadManager:downloadManager forResponse:response];
}]
map:^(SQRLDownload *download) {
return download.fileURL;
}]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred];
return [[[[data
map:^(NSData *bodyData) {
return [[downloadURL
try:^(NSURL *fileURL, NSError **errorRef) {
return [self appendData:bodyData toURL:fileURL error:errorRef];
}]
ignoreValues];
}]
concat]
ignoreValues]
concat:downloadURL];
}];
}]
setNameWithFormat:@"%@ download: %@", self, downloadManager];
}
@end
+10 -26
View File
@@ -14,8 +14,6 @@ NSString * const SQRLUpdateJSONReleaseNotesKey = @"notes";
NSString * const SQRLUpdateJSONNameKey = @"name";
NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
@implementation SQRLUpdate
#pragma mark Lifecycle
@@ -52,7 +50,7 @@ NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
@@ -63,10 +61,10 @@ NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@keypath(SQRLUpdate.new, releaseNotes): SQRLUpdateJSONReleaseNotesKey,
@keypath(SQRLUpdate.new, releaseName): SQRLUpdateJSONNameKey,
@keypath(SQRLUpdate.new, releaseDate): SQRLUpdateJSONPublicationDateKey,
@keypath(SQRLUpdate.new, updateURL): SQRLUpdateJSONURLKey,
@keypath(SQRLUpdate.new, releaseNotes): @"notes",
@keypath(SQRLUpdate.new, releaseName): @"name",
@keypath(SQRLUpdate.new, releaseDate): @"pub_date",
@keypath(SQRLUpdate.new, updateURL): @"url",
};
}
@@ -96,8 +94,8 @@ NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
}
// If neither match, try removing the ':' in the time zone
static NSRegularExpression *timeZoneSuffix;
static dispatch_once_t timeZoneSuffixPredicate;
static NSRegularExpression *timeZoneSuffix = nil;
static dispatch_once_t timeZoneSuffixPredicate = 0;
dispatch_once(&timeZoneSuffixPredicate, ^ {
timeZoneSuffix = [NSRegularExpression regularExpressionWithPattern:@"([-+])([0-9]{2}):([0-9]{2})$" options:0 error:NULL];
});
@@ -136,30 +134,16 @@ NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
- (BOOL)validateUpdateURL:(NSURL **)updateURLPtr error:(NSError **)error {
NSURL *updateURL = *updateURLPtr;
if (![updateURL isKindOfClass:NSURL.class]) {
if (![updateURL isKindOfClass:NSURL.class] || updateURL.scheme == nil || updateURL.host == nil || updateURL.path == nil) {
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;
}
+12 -49
View File
@@ -6,35 +6,6 @@
// 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;
@@ -43,7 +14,7 @@ extern NSString * const SQRLUpdaterServerDataErrorKey;
// error with code `SQRLUpdaterErrorInvalidJSON` is generated.
extern NSString * const SQRLUpdaterJSONObjectErrorKey;
@class RACAction;
@class RACCommand;
@class RACDisposable;
@class RACSignal;
@@ -52,14 +23,21 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Kicks off a check for updates.
//
// If an update is available, it will be sent on
// `[checkForUpdatesAction deferred]` and `updates` once downloaded.
@property (nonatomic, strong, readonly) RACAction *checkForUpdatesAction;
// If an update is available, it will be sent on `updates` once downloaded.
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
// update is available. Completes when the receiver deallocates.
// update is available.
//
// This signal is actually just `checkForUpdatesCommand.executionSignals`,
// flattened for convenience.
@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
@@ -97,21 +75,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// checking.
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval;
// Terminates the running application to install any available update, then
// automatically relaunches the app after updating.
//
// This method is only useful if you want the application to automatically
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
// other exit mechanism.
//
// After invoking this method, the receiver is responsible for terminating the
// application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
// wrong before termination. The signal will never complete.
- (RACSignal *)relaunchToInstallUpdate;
@end
@interface SQRLUpdater (Unavailable)
+157 -256
View File
@@ -17,30 +17,16 @@
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLUpdate.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLZipArchiver.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SQRLURLConnection.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.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;
@interface SQRLUpdater ()
// The directory manager used for downloads and unpacks.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
@@ -69,25 +55,22 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// errors, on a background thread.
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update;
// Downloads the update archive for the given update.
// Downloads the archived bundle associated with the given update.
//
// update - Describes the update to install. This must not be nil.
// downloadDirectory - A directory in which to create a temporary directory for
// this download. This must not be nil.
// downloadDirectory - A directory in which to create a temporary directory for this
// download. This must not be nil.
//
// Returns a signal which sends an NSURL of the downloaded update archive then
// completes, or errors, on a background thread.
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
// Returns a signal which sends an unarchived `NSBundle` then completes, or
// errors, on a background thread.
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
// Unpacks an update archive.
// Creates a unique directory in which to save the update bundle, for later use
// by ShipIt.
//
// archiveURL - The location of the update archive, currently only ZIP
// archives are supported. Must not be nil.
// directoryURL - A directory in which to place the unpacked contents of the
// archive. Must not be nil.
//
// Returns a signal which completes or errors.
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL;
// Returns a signal which sends an `NSURL` then completes, or errors, on an
// unspecified thread.
- (RACSignal *)uniqueTemporaryDirectoryForUpdate;
// Recursively searches the given directory for an application bundle that has
// the same identifier as the running application.
@@ -120,27 +103,16 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// 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 {
RACSubject *_updates;
}
@implementation SQRLUpdater
#pragma mark Properties
- (RACSignal *)updates {
return [_updates
deliverOn:RACScheduler.mainThreadScheduler];
return [[self.checkForUpdatesCommand.executionSignals
concat]
setNameWithFormat:@"%@ -updates", self];
}
#pragma mark Lifecycle
@@ -159,29 +131,39 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
_updateRequest = [updateRequest copy];
_updateClass = SQRLUpdate.class;
_directoryManager = SQRLDirectoryManager.currentApplicationManager;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_checkForUpdatesAction = [[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
flattenMap:^(NSURLRequest *request) {
@strongify(self);
return [self checkForUpdates:request];
}]
action];
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
_updates = [[RACSubject
subject]
setNameWithFormat:@"%@ updates", self];
// TODO: Maybe allow this to be an argument to the command?
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
_shipItLauncher = [[[[RACSignal
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:request]
reduceEach:^(id _, NSData *data) {
return data;
}]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doError:^(id _) {
self.shouldRelaunch = NO;
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
_shipItLauncher = [[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
@@ -193,80 +175,31 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
replayLazily]
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.checkForUpdatesAction
deferred]
ignoreValues]
return [[self.checkForUpdatesCommand
execute:RACUnit.defaultUnit]
catch:^(NSError *error) {
NSLog(@"Error checking for updates: %@", error);
return [RACSignal empty];
}];
}]
takeUntil:self.rac_willDeallocSignal]
subscribeCompleted:^{}];
}
- (RACSignal *)checkForUpdates:(NSURLRequest *)request {
NSParameterAssert(request != nil);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
if (updatesDisabled) return [RACSignal empty];
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:newRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
publish]
connect];
}
- (RACSignal *)updateFromJSONData:(NSData *)data {
@@ -283,7 +216,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
userInfo[SQRLUpdaterServerDataErrorKey] = data;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:userInfo]];
}
Class updateClass = self.updateClass;
@@ -300,7 +233,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
userInfo[SQRLUpdaterJSONObjectErrorKey] = JSON;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorInvalidJSON userInfo:userInfo]];
}
return [RACSignal return:update];
@@ -308,21 +241,14 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
setNameWithFormat:@"%@ -updateFromJSONData:", self];
}
#pragma Download
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update {
NSParameterAssert(update != nil);
return [[[self.directoryManager
createUniqueUpdateDirectoryURL]
return [[[self
uniqueTemporaryDirectoryForUpdate]
flattenMap:^(NSURL *downloadDirectory) {
return [[[[[[self
downloadArchiveForUpdate:update intoDirectory:downloadDirectory]
flattenMap:^(NSURL *updateArchiveURL) {
return [self unpackArchiveAtURL:updateArchiveURL intoDirectory:downloadDirectory];
}]
ignoreValues]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
return [[[self
downloadBundleForUpdate:update intoDirectory:downloadDirectory]
flattenMap:^(NSBundle *updateBundle) {
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
}]
@@ -336,65 +262,74 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
}
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
NSParameterAssert(update != nil);
NSParameterAssert(downloadDirectory != nil);
return [[[RACSignal
return [[[[[RACSignal
defer:^{
NSURL *zipDownloadURL = update.updateURL;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:zipDownloadRequest];
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:self.directoryManager];
return [[[[connection
download:downloadManager]
reduceEach:^(NSURLResponse *response, NSURL *bodyLocation) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [[RACSignal
return:bodyLocation]
tryMap:^ NSURL * (NSURL *bodyLocation, NSError **errorRef) {
NSURL *updateLocation = [downloadDirectory URLByAppendingPathComponent:response.suggestedFilename];
if (![NSFileManager.defaultManager moveItemAtURL:bodyLocation toURL:updateLocation error:errorRef]) return nil;
return updateLocation;
}];
return [[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(id _, NSData *data) {
return data;
}]
flatten]
concat:[[downloadManager
removeAllResumableDownloads]
catchTo:RACSignal.empty]];
flattenMap:^(NSData *data) {
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
NSError *error = nil;
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal return:zipOutputURL];
} else {
return [RACSignal error:error];
}
}];
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
setNameWithFormat:@"%@ -downloadArchiveForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
flattenMap:^(NSURL *zipOutputURL) {
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
}]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
#pragma mark File Management
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL {
NSParameterAssert(archiveURL != nil);
NSParameterAssert(directoryURL != nil);
return [[RACSignal
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
return [[[RACSignal
defer:^{
return [SQRLZipArchiver unzipArchiveAtURL:archiveURL intoDirectoryAtURL:directoryURL];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
setNameWithFormat:@"%@ unpackArchiveAtURL: %@ intoDirectory: %@", self, archiveURL, directoryURL];
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
};
if (mkdtemp(updateDirectoryCString) == NULL) {
int code = errno;
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not create temporary directory", nil),
NSURLErrorKey: updateDirectoryTemplate
};
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
}
NSString *updateDirectoryPath = [NSFileManager.defaultManager stringWithFileSystemRepresentation:updateDirectoryCString length:strlen(updateDirectoryCString)];
return [RACSignal return:[NSURL fileURLWithPath:updateDirectoryPath isDirectory:YES]];
}]
setNameWithFormat:@"%@ -uniqueTemporaryDirectoryForUpdate", self];
}
- (RACSignal *)updateBundleMatchingCurrentApplicationInDirectory:(NSURL *)directory {
@@ -407,28 +342,25 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
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;
}
NSURL *updateBundleURL = [[[enumerator.rac_promise
start]
filter:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
return NO;
}
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
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];
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}];
if (updateBundleURL != nil) {
return [RACSignal return:updateBundleURL];
@@ -437,7 +369,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Could not locate update bundle for %@ within %@", nil), NSRunningApplication.currentApplication.bundleIdentifier, directory],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLUpdaterErrorMissingUpdateBundle userInfo:userInfo]];
}
}]
map:^(NSURL *URL) {
@@ -446,15 +378,6 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
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 {
@@ -463,75 +386,53 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
return [[[[self.signature
verifyBundleAtURL:updateBundle.bundleURL]
concat:[RACSignal return:[[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle]]]
then:^{
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle];
return [RACSignal return:downloadedUpdate];
}]
flattenMap:^(SQRLDownloadedUpdate *downloadedUpdate) {
return [[self
prepareUpdateForInstallation:downloadedUpdate]
concat:[RACSignal return:downloadedUpdate]];
return [[self prepareUpdateForInstallation:downloadedUpdate] then:^{
return [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 [[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
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;
}]
ignoreValues]
concat:[RACSignal defer:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
return [state writeUsingURL:self.shipItStateURL];
}]]
concat:self.shipItLauncher]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing update", nil) description:NSLocalizedString(@"An update for %@ is being prepared. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier]
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
flattenMap:^(SQRLShipItState *state) {
state.relaunchAfterInstallation = YES;
return [[state
writeUsingURL:self.shipItStateURL]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
}]
deliverOn:RACScheduler.mainThreadScheduler]
doCompleted:^{
[NSApp terminate:self];
}]
// Never allow `completed` to escape this signal chain (in case
// -terminate: is asynchronous or something crazy).
concat:[RACSignal never]]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
@end
-7
View File
@@ -10,17 +10,10 @@
@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
+15 -14
View File
@@ -9,10 +9,9 @@
#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;
@@ -130,23 +129,25 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
// This is important because the signals on `self` complete upon
// dealloc.
return:self]
ignoreValues]
concat:[RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
then:^{
return [RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}]]
return [RACSignal error:[NSError errorWithDomain:SQRLErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}];
}]
take:1]
flatten]
replay]
setNameWithFormat:@"-launchWithArguments: %@", arguments];
self.dittoTask.arguments = arguments;
+15 -43
View File
@@ -24,9 +24,6 @@
// 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) {
@@ -55,15 +52,16 @@ 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);
}]
ignoreValues]
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
concat:[SQRLShipItState readUsingURL:stateLocation]]
then:^{
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
return [SQRLShipItState readUsingURL:stateLocation];
}]
catch:^(NSError *error) {
NSLog(@"Error reading saved installer state: %@", error);
@@ -75,14 +73,11 @@ 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) {
action = [[RACSignal
defer:^{
return [[[installer.abortInstallationCommand
execute:state]
initially:^{
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);
@@ -91,8 +86,9 @@ int main(int argc, const char * argv[]) {
return [RACSignal empty];
}];
} else {
action = [[[[RACSignal
defer:^{
return [[[[[state
writeUsingURL:stateLocation]
initially:^{
if (freshInstall) {
NSLog(@"Beginning installation");
state.installerState = SQRLInstallerStateClearingQuarantine;
@@ -101,39 +97,15 @@ int main(int argc, const char * argv[]) {
}
state.installationStateAttempt = attempt;
return [state writeUsingURL:stateLocation];
}]
concat:[installer installUpdateWithState:state]]
then:^{
return [installer.installUpdateCommand execute: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);
+71
View File
@@ -0,0 +1,71 @@
//
// 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;
+11
View File
@@ -0,0 +1,11 @@
//
// 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";
@@ -1,23 +0,0 @@
//
// NSHTTPURLResponseExtensionsSpec.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "NSHTTPURLResponse+SQRLExtensions.h"
SpecBegin(NSHTTPURLResponseExtensions)
it(@"should perform case insensitive key look up", ^{
NSDictionary *headers = @{
@"ETag": @"foo",
};
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://localhost"] statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:headers];
expect([response sqrl_valueForHTTPHeaderField:@"etag"]).to.equal(@"foo");
expect([response sqrl_valueForHTTPHeaderField:@"ETAG"]).to.equal(@"foo");
});
SpecEnd
+11 -10
View File
@@ -7,6 +7,7 @@
//
#import "SQRLCodeSignature.h"
#import "Squirrel-Constants.h"
SpecBegin(SQRLCodeSignature)
@@ -54,7 +55,7 @@ describe(@"code signature changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -66,7 +67,7 @@ describe(@"code signature changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -80,7 +81,7 @@ describe(@"main executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
});
@@ -92,7 +93,7 @@ describe(@"main executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -113,7 +114,7 @@ describe(@"helper executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -125,7 +126,7 @@ describe(@"helper executable changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -146,7 +147,7 @@ describe(@"resource changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -158,7 +159,7 @@ describe(@"resource changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -178,7 +179,7 @@ describe(@"framework changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
@@ -190,7 +191,7 @@ describe(@"framework changes", ^{
expect(success).to.beFalsy();
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
+1 -5
View File
@@ -60,16 +60,12 @@ 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];
@@ -110,7 +106,7 @@ it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
xit(@"should deep verify after signing", ^{
it(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
-87
View File
@@ -1,87 +0,0 @@
//
// SQRLDownloadControllerSpec.m
// Squirrel
//
// Created by Keith Duncan on 27/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
#import "NSError+SQRLVerbosityExtensions.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
SpecBegin(SQRLDownloadManager)
__block SQRLDownloadManager *downloadManager = nil;
beforeAll(^{
downloadManager = SQRLDownloadManager.defaultDownloadManager;
NSError *removeError = nil;
BOOL remove = [[downloadManager removeAllResumableDownloads] waitUntilCompleted:&removeError];
if (!remove) {
if ([removeError.domain isEqualToString:NSCocoaErrorDomain] && removeError.code == NSFileNoSuchFileError) return;
NSLog(@"Couldn’t remove resumable downloads %@", removeError.sqrl_verboseDescription);
}
});
NSURL * (^newTestURL)() = ^ () {
return [[NSURL alloc] initWithScheme:@"http" host:@"localhost" path:[@"/" stringByAppendingString:NSProcessInfo.processInfo.globallyUniqueString]];
};
NSURL * (^newDownloadURL)() = ^ () {
NSError *error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:newTestURL()]] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSURL *downloadURL = download.fileURL;
expect(downloadURL).notTo.beNil();
return downloadURL;
};
it(@"should return a file that doesn't exist yet for new URLs", ^{
NSURL *downloadURL = newDownloadURL();
BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:downloadURL.path];
expect(exists).to.beFalsy();
});
it(@"should return a path in a writable directory for new URLs", ^{
NSURL *downloadURL = newDownloadURL();
BOOL writable = [NSFileManager.defaultManager isWritableFileAtPath:downloadURL.URLByDeletingLastPathComponent.path];
expect(writable).to.beTruthy();
});
it(@"should return the same path for the same URL", ^{
NSURL *testURL = newTestURL();
SQRLDownload *download1 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
SQRLDownload *download2 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
expect(download1).notTo.beNil();
expect(download1).to.equal(download2);
});
it(@"should remember a response", ^{
NSURLRequest *request = [NSURLRequest requestWithURL:newTestURL()];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:@{ @"ETag": NSProcessInfo.processInfo.globallyUniqueString }];
SQRLDownload *initialDownload = [[downloadManager downloadForRequest:request] first];
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:request response:response fileURL:initialDownload.fileURL];
expect(newDownload).notTo.equal(initialDownload);
NSError *error = nil;
BOOL setNewDownload = [[downloadManager setDownload:newDownload forRequest:request] waitUntilCompleted:&error];
expect(setNewDownload).to.beTruthy();
expect(error).to.beNil();
SQRLResumableDownload *resumedDownload = [[downloadManager downloadForRequest:request] first];
expect(newDownload).to.equal(resumedDownload);
});
SpecEnd
-40
View File
@@ -1,40 +0,0 @@
//
// SQRLDownloadedUpdateSpec.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLDownloadedUpdate)
it(@"should serialise to JSON", ^{
NSError *error = nil;
SQRLUpdate *update = [SQRLUpdate modelWithDictionary:@{
@keypath(SQRLUpdate.new, updateURL) : [NSURL URLWithString:@"http://fake.domain/path"],
} error:&error];
expect(update).notTo.beNil();
NSURL *testApplicationURL = [self createTestApplicationUpdate];
NSBundle *testApplicationBundle = [NSBundle bundleWithURL:testApplicationURL];
expect(testApplicationBundle).notTo.beNil();
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:testApplicationBundle];
expect(downloadedUpdate).notTo.beNil();
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:downloadedUpdate];
expect(JSONDictionary).notTo.beNil();
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
expect(JSONData).notTo.beNil();
expect(error).to.beNil();
JSONDictionary = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:&error];
expect(JSONDictionary).notTo.beNil();
SQRLDownloadedUpdate *downloadedUpdate2 = [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:&error];
expect(downloadedUpdate2).to.equal(downloadedUpdate);
expect(error).to.beNil();
});
SpecEnd
-18
View File
@@ -1,18 +0,0 @@
//
// SQRLHTTPServer.h
// Squirrel
//
// Created by Keith Duncan on 03/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface SQRLHTTPServer : NSObject
- (NSURL *)start:(NSError **)errorRef;
- (void)invalidate;
@property (copy, nonatomic) CFHTTPMessageRef (^responseBlock)(CFHTTPMessageRef request);
@end
-165
View File
@@ -1,165 +0,0 @@
//
// SQRLHTTPServer.m
// Squirrel
//
// Created by Keith Duncan on 03/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLHTTPServer.h"
#import <sys/socket.h>
#import <netinet/in.h>
@interface SQRLHTTPServer ()
@property (assign, nonatomic) dispatch_source_t listenerSource;
@property (strong, nonatomic) NSMutableArray *cleanupBlocks;
@end
@implementation SQRLHTTPServer
- (id)init {
self = [super init];
if (self == nil) return nil;
_cleanupBlocks = [[NSMutableArray alloc] init];
return self;
}
- (void)dealloc {
[self invalidate];
}
- (NSURL *)start:(NSError **)errorRef {
NSParameterAssert(self.listenerSource == NULL);
int listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in listenAddress = {
.sin_len = sizeof(listenAddress),
.sin_family = AF_INET,
.sin_port = 0,
.sin_addr = {
.s_addr = htonl(INADDR_LOOPBACK),
},
};
int bindError = bind(listenSocket, (struct sockaddr const *)&listenAddress, listenAddress.sin_len);
if (bindError != 0) {
close(listenSocket);
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:bindError userInfo:nil];
return nil;
}
listen(listenSocket, 128);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listenSocket, 0, NULL);
dispatch_source_set_event_handler(source, ^{
int connectionSocket = accept(listenSocket, NULL, NULL);
// Wait to close the connection until after the test case is complete.
// We need NSURLConnection to timeout, closing the connection causes it
// to error immediately.
[self addCleanupBlock:^{
close(connectionSocket);
}];
CFHTTPMessageRef request = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true));
while (1) {
uint8_t buffer;
ssize_t readLength = read(connectionSocket, &buffer, 1);
if (readLength < 0) {
return;
}
CFHTTPMessageAppendBytes(request, (UInt8 const *)&buffer, readLength);
if (!CFHTTPMessageIsHeaderComplete(request)) {
continue;
}
// Doesn't support reading requests with a body for simplicity
NSString *contentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Content-Length")));
if (contentLength != nil) {
return;
}
break;
}
NSLog(@"Received Request:");
NSLog(@"%@", [[NSString alloc] initWithData:CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)) encoding:NSASCIIStringEncoding]);
CFHTTPMessageRef response = self.responseBlock(request);
NSData *responseData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(response));
NSLog(@"Sending Response:");
NSLog(@"%@", [[NSString alloc] initWithData:responseData encoding:NSASCIIStringEncoding]);
size_t bufferLength = responseData.length;
uint8_t const *buffer = responseData.bytes;
while (1) {
ssize_t writeLength = write(connectionSocket, buffer, bufferLength);
if (writeLength < 0) {
return;
}
buffer += writeLength;
bufferLength -= writeLength;
if (bufferLength == 0) {
break;
}
}
});
dispatch_source_set_cancel_handler(source, ^{
close(listenSocket);
});
dispatch_resume(source);
struct sockaddr_storage localAddress = {};
socklen_t localAddressLength = sizeof(localAddress);
int localAddressError = getsockname(listenSocket, (struct sockaddr *)&localAddress, &localAddressLength);
if (localAddressError != 0) {
dispatch_release(source);
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:localAddressError userInfo:nil];
return nil;
}
self.listenerSource = source;
// IPv4 and IPv6 address transport layer port fields are at the same offset
// and are the same size
in_port_t port = ntohs(((struct sockaddr_in *)&localAddress)->sin_port);
return [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%ld/", (unsigned long)port]];
}
- (void)invalidate {
dispatch_source_t listenerSource = self.listenerSource;
if (listenerSource == NULL) return;
dispatch_source_cancel(listenerSource);
dispatch_release(listenerSource);
self.listenerSource = NULL;
[self performCleanup];
}
- (void)addCleanupBlock:(void (^)(void))block {
[self.cleanupBlocks addObject:[block copy]];
}
- (void)performCleanup {
NSArray *blocks = [self.cleanupBlocks copy];
[self.cleanupBlocks removeAllObjects];
for (void (^currentBlock)(void) in blocks) {
currentBlock();
}
}
@end
+6 -31
View File
@@ -77,7 +77,7 @@ it(@"should install an update in process", ^{
expect(installer).notTo.beNil();
NSError *installError = nil;
BOOL install = [[installer installUpdateWithState:state] asynchronouslyWaitUntilCompleted:&installError];
BOOL install = [[installer.installUpdateCommand execute:state] asynchronouslyWaitUntilCompleted:&installError];
expect(install).to.beTruthy();
expect(installError).to.beNil();
});
@@ -104,28 +104,6 @@ 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;
@@ -139,9 +117,6 @@ 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;
@@ -161,26 +136,26 @@ describe(@"signal handling", ^{
});
it(@"should handle SIGHUP", ^{
system("killall -HUP ShipIt");
system("killall -v -HUP ShipIt");
});
it(@"should handle SIGTERM", ^{
system("killall -TERM ShipIt");
system("killall -v -TERM ShipIt");
});
it(@"should handle SIGINT", ^{
system("killall -INT ShipIt");
system("killall -v -INT ShipIt");
});
it(@"should handle SIGQUIT", ^{
system("killall -QUIT ShipIt");
system("killall -v -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 -KILL ShipIt");
system("killall -v -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
+3 -6
View File
@@ -40,8 +40,7 @@ it(@"should wait until one instance terminates", ^{
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
[app forceTerminate];
expect(app.terminated).will.beTruthy();
expect([app terminate]).to.beTruthy();
expect(completed).will.beTruthy();
});
@@ -56,12 +55,10 @@ it(@"should wait until multiple instances terminate", ^{
expect(completed).to.beFalsy();
[app1 forceTerminate];
expect(app1.terminated).will.beTruthy();
expect([app1 terminate]).to.beTruthy();
expect(completed).to.beFalsy();
[app2 forceTerminate];
expect(app2.terminated).will.beTruthy();
expect([app2 terminate]).to.beTruthy();
expect(completed).will.beTruthy();
});
-7
View File
@@ -88,11 +88,4 @@ 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
+25 -45
View File
@@ -51,10 +51,6 @@ 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
@@ -75,27 +71,27 @@ static void SQRLSignalHandler(int sig) {
@"otest-x86_64.ShipIt",
];
RACSignal *logLocations = [[[[folders.rac_signal
flattenMap:^(NSString *folder) {
return [folder stringsByAppendingPaths:@[
@"ShipIt_stdout.log",
@"ShipIt_stderr.log",
]].rac_signal;
}]
map:^(NSString *path) {
return [appSupportURL URLByAppendingPathComponent:path];
}]
doNext:^(NSURL *location) {
[[NSData data] writeToURL:location atomically:YES];
}]
map:^(NSURL *location) {
return location.path;
}];
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;
}];
NSTask *readShipIt = [self tailTaskWithPaths:logLocations];
[readShipIt launch];
for (NSURL *URL in URLs) {
[[NSData data] writeToURL:URL atomically:YES];
}
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
NSArray *args = [[[URLs
map:^(NSURL *URL) {
return URL.path;
}]
startWith:@"-f"]
array];
NSTask *readShipIt = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:args];
NSAssert([readShipIt isRunning], @"Could not start task %@ with arguments: %@", readShipIt, args);
atexit_b(^{
[readShipIt terminate];
@@ -135,24 +131,6 @@ 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 {
@@ -198,9 +176,7 @@ static void SQRLSignalHandler(int sig) {
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
[[NSData data] writeToURL:testAppLog atomically:YES];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSignal return:testAppLog.path]];
[readTestApp launch];
NSTask *readTestApp = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:@[ @"-f", testAppLog.path ]];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
@@ -325,7 +301,11 @@ static void SQRLSignalHandler(int sig) {
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
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);
}
NSError *lookupError = nil;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
-311
View File
@@ -1,311 +0,0 @@
//
// SQRLURLConnectionSpec.m
// Squirrel
//
// Created by Keith Duncan on 27/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
#import <ReactiveCocoa/EXTScope.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "SQRLHTTPServer.h"
SpecBegin(SQRLURLConnection);
__block SQRLDownloadManager *downloadManager;
beforeAll(^{
downloadManager = SQRLDownloadManager.defaultDownloadManager;
NSError *removeError = nil;
BOOL remove = [[downloadManager
removeAllResumableDownloads]
waitUntilCompleted:&removeError];
expect(remove).to.beTruthy();
expect(removeError).to.beNil();
});
beforeEach(^{
Expecta.asynchronousTestTimeout = 60.;
});
afterAll(^{
Expecta.asynchronousTestTimeout = 1.;
});
describe(@"file scheme URLs", ^{
NSURL *fileLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"test"];
NSData *testContents = [@"test" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
BOOL write = [testContents writeToURL:fileLocation options:0 error:&error];
expect(write).to.beTruthy();
expect(error).to.beNil();
NSURLRequest *request = [NSURLRequest requestWithURL:fileLocation];
__block SQRLURLConnection *connection;
beforeEach(^{
connection = [[SQRLURLConnection alloc] initWithRequest:request];
});
void (^testBodyEqualTestContents)(NSData *) = ^ (NSData *body) {
expect(body.length).to.equal(testContents.length);
expect(body).to.equal(testContents);
};
it(@"should retrieve file:// scheme URLs", ^{
NSError *error;
RACTuple *result = [[connection retrieve] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
testBodyEqualTestContents(result[1]);
});
it(@"should download file:// scheme URLs", ^{
NSError *error;
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
NSData *body = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(error).to.beNil();
testBodyEqualTestContents(body);
});
});
static NSData * (^stringTimes)(NSString *, NSUInteger) = ^ (NSString *string, NSUInteger times) {
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *data = [NSMutableData dataWithCapacity:(stringData.length + 2) * times];
for (NSUInteger idx = 0; idx < times; idx++) {
[data appendData:stringData];
[data appendData:[NSData dataWithBytes:"\r\n" length:2]];
}
return data;
};
static SQRLHTTPServer * (^newHttpServer)(SQRLTestCase *) = ^ (SQRLTestCase *self) {
SQRLHTTPServer *server = [[SQRLHTTPServer alloc] init];
[self addCleanupBlock:^{
[server invalidate];
}];
return server;
};
it(@"should resume a download", ^{
// Start server
SQRLHTTPServer *server = newHttpServer(self);
NSError *error = nil;
NSURL *baseURL = [server start:&error];
expect(baseURL).notTo.beNil();
expect(error).to.beNil();
error = nil;
// Prepare half response
NSData *firstHalf = stringTimes(@"the quick brown fox", 100);
NSData *secondHalf = stringTimes(@"jumped over the lazy doge", 100);
NSString *ETag = NSProcessInfo.processInfo.globallyUniqueString;
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
NSDictionary *responseHeaders = @{
@"Content-Length": [@(firstHalf.length + secondHalf.length) stringValue],
@"ETag": ETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstHalf);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.beNil();
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.beNil();
return serverResponse;
};
// Issue first request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
request.timeoutInterval = 1.;
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).to.beNil();
expect(error).notTo.beNil();
error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
expect(downloadedData.length).to.equal(firstHalf.length);
expect(downloadedData).to.equal(firstHalf);
expect(error).to.beNil();
// Prepare remainder response
serverResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 206, NULL, kCFHTTPVersion1_1);
responseHeaders = @{
@"Content-Length": [@(secondHalf.length) stringValue],
@"Range": [NSString stringWithFormat:@"%lu-%lu", firstHalf.length, secondHalf.length - 1],
@"ETag": ETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondHalf);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.equal(ETag);
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstHalf.length]));
return serverResponse;
};
// Issue second request
connection = [[SQRLURLConnection alloc] initWithRequest:request];
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
NSMutableData *fullBody = [firstHalf mutableCopy];
[fullBody appendData:secondHalf];
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(downloadedData.length).to.equal(fullBody.length);
expect(downloadedData).to.equal(fullBody);
expect(error).to.beNil();
});
it(@"should not resume downloads for a response with a different ETag", ^{
// Start server
SQRLHTTPServer *server = newHttpServer(self);
NSError *error = nil;
NSURL *baseURL = [server start:&error];
expect(baseURL).notTo.beNil();
expect(error).to.beNil();
error = nil;
// Prepare first response
NSData *firstBody = stringTimes(@"the quick brown fox", 100);
NSString *firstETag = NSProcessInfo.processInfo.globallyUniqueString;
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
NSDictionary *responseHeaders = @{
// Claim the response to be longer so that the connection will timeout
// waiting for the extra bytes
@"Content-Length": [@(firstBody.length + 100) stringValue],
@"ETag": firstETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstBody);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.beNil();
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.beNil();
return serverResponse;
};
// Issue first request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
request.timeoutInterval = 1.;
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).to.beNil();
expect(error).notTo.beNil();
error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
expect(downloadedData.length).to.equal(firstBody.length);
expect(downloadedData).to.equal(firstBody);
expect(error).to.beNil();
// Prepare second response
NSData *secondBody = stringTimes(@"jumps over the lazy doge", 100);
NSString *secondETag = NSProcessInfo.processInfo.globallyUniqueString;
serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
responseHeaders = @{
@"Content-Length": [@(secondBody.length) stringValue],
@"ETag": secondETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondBody);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.equal(firstETag);
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstBody.length]));
return serverResponse;
};
// Issue second request
connection = [[SQRLURLConnection alloc] initWithRequest:request];
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(downloadedData.length).to.equal(secondBody.length);
expect(downloadedData).to.equal(secondBody);
expect(error).to.beNil();
});
SpecEnd
+2 -6
View File
@@ -39,17 +39,13 @@ it(@"should validate release name and notes", ^{
});
it(@"should parse Central style dates", ^{
NSError *error = nil;
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:&error];
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:NULL];
expect(update.releaseDate).notTo.beNil();
expect(error).to.beNil();
});
it(@"should parse ISO 8601 dates", ^{
NSError *error = nil;
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:&error];
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:NULL];
expect(update.releaseDate).notTo.beNil();
expect(error).to.beNil();
});
SpecEnd
+1 -132
View File
@@ -8,10 +8,6 @@
#import "SQRLTestUpdate.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "SQRLDirectoryManager.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
SpecBegin(SQRLUpdater)
@@ -55,14 +51,10 @@ beforeEach(^{
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
} error:NULL];
writeUpdate(update);
@@ -138,127 +130,4 @@ it(@"should use the application's bundled version of Squirrel and update in-plac
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
before(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
it(@"should return an error for non 2xx code HTTP responses", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:nil statusCode:/* Server Error */ 500 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
});
it(@"should return an error for non JSON data", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:NSData.data statusCode:/* OK */ 200 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
});
});
it(@"should clean up resumable downloads after a successful download", ^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication"];
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:directoryManager];
NSError *error = nil;
BOOL remove = [[downloadManager removeAllResumableDownloads] asynchronouslyWaitUntilCompleted:&error];
expect(remove).to.beTruthy();
expect(error).to.beNil();
NSURL *updateApplicationBundle = [self createTestApplicationUpdate];
NSURL *firstDownload = zipUpdate(updateApplicationBundle);
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:firstDownload]];
BOOL download = [[connection download:downloadManager] asynchronouslyWaitUntilCompleted:&error];
expect(download).to.beTruthy();
expect(error).to.beNil();
NSURL *downloadDirectory = [[directoryManager downloadDirectoryURL] firstOrDefault:nil success:NULL error:&error];
expect(downloadDirectory).notTo.beNil();
expect(error).to.beNil();
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
expect(contents.count).to.equal(1);
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
@"updateURL": zipUpdate(updateApplicationBundle),
@"final": @YES,
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
if (contents == nil) {
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSFileReadNoSuchFileError);
} else {
expect(contents.count).to.equal(0);
}
});
it(@"should unpack update archives into a .noindex directory", ^{
NSURL *zippedUpdateLocation = zipUpdate([self createTestApplicationUpdate]);
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
@keypath(update.updateURL): zippedUpdateLocation,
} error:NULL];
writeUpdate(update);
__block SQRLDownloadedUpdate *downloadedUpdate;
[[[[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil]
map:^(NSNotification *notification) {
return notification.userInfo[@"update"];
}]
tryMap:^(NSString *serialisedUpdate, NSError **errorRef) {
NSData *JSONData = [serialisedUpdate dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:errorRef];
}]
tryMap:^(NSDictionary *JSONDictionary, NSError **errorRef) {
return [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:errorRef];
}]
subscribeNext:^(SQRLDownloadedUpdate *update) {
downloadedUpdate = update;
}];
launchWithEnvironment(nil);
expect(downloadedUpdate).willNot.beNil();
NSURL *bundleURL = downloadedUpdate.bundle.bundleURL;
expect([bundleURL.path rangeOfString:@".noindex"].location).notTo.equal(NSNotFound);
});
SpecEnd
+3 -2
View File
@@ -8,8 +8,9 @@
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
#import "Squirrel-Constants.h"
SpecBegin(SQRLZipOperation)
SpecBegin(SQRLZipArchiver)
it(@"should extract a zip archive created by the Finder", ^{
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
@@ -35,7 +36,7 @@ it(@"should fail to extract a nonexistent zip archive", ^{
NSLog(@"%@", error);
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.domain).to.equal(SQRLErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
});
+5 -10
View File
@@ -10,7 +10,6 @@
#import "SQRLDirectoryManager.h"
#import "SQRLShipItLauncher.h"
#import "SQRLTestUpdate.h"
#import "SQRLDownloadedUpdate.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@@ -71,22 +70,18 @@
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
updateCheckCount++;
return [self.updater.checkForUpdatesAction deferred];
return [self.updater.checkForUpdatesCommand execute:RACUnit.defaultUnit];
}]
doNext:^(SQRLDownloadedUpdate *update) {
NSLog(@"Got a candidate update: %@", update);
NSString *serialisedUpdate = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[MTLJSONAdapter JSONDictionaryFromModel:update] options:0 error:NULL] encoding:NSUTF8StringEncoding];
[NSDistributedNotificationCenter.defaultCenter postNotificationName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil userInfo:@{ @"update": serialisedUpdate }];
}]
// Retry until we get the expected release.
repeat]
skipWhile:^ BOOL (SQRLDownloadedUpdate *download) {
skipUntilBlock:^(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 _) {
@@ -97,12 +92,12 @@
NSLog(@"Error in updater: %@", error);
return [RACSignal empty];
}]
concat:[RACSignal defer:^{
then:^{
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];
}];
+6 -68
View File
@@ -1,66 +1,6 @@
# 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:
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:
```
$ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git
@@ -68,15 +8,13 @@ $ 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 merge in upstream changes later:
To bring in upstream changes later:
```
$ git fetch -p objc-build-scripts
$ git merge --ff --squash -Xsubtree=script objc-build-scripts/master
$ git merge -Xsubtree=script objc-build-scripts/master
```
[xctool]: https://github.com/facebook/xctool
+7 -69
View File
@@ -1,73 +1,11 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
##
## Configuration Variables
##
set -o errexit
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
echo "*** Updating submodules..."
git submodule sync --quiet
git submodule update --init
git submodule foreach --recursive --quiet "git submodule sync --quiet && git submodule update --init"
+80 -113
View File
@@ -1,162 +1,129 @@
#!/bin/bash
export SCRIPT_DIR=$(dirname "$0")
SCRIPT_DIR=$(dirname "$0")
cd "$SCRIPT_DIR/.."
##
## Configuration Variables
##
SCHEMES="$@"
# The build configuration to use.
if [ -z "$XCCONFIGURATION" ]
then
XCCONFIGURATION="Test"
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")}
# 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
# 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 bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
BOOTSTRAP="$SCRIPT_DIR/bootstrap"
# A bootstrap script to run before building.
#
# If this file does not exist, it is not considered an error.
: ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"}
# 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
# 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
}
# Extra build settings to pass to xcodebuild.
XCODEBUILD_SETTINGS="TEST_AFTER_BUILD=YES"
##
## Code Signing
## Code-signing Setup
##
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
}
# 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
##
## Build Process
##
main ()
{
config
unlock_keychain
if [ -f "$BOOTSTRAP" ]
if [ -z "$*" ]
then
# lol recursive shell script
if [ -n "$DEFAULT_TARGETS" ]
then
echo "*** Bootstrapping..."
"$BOOTSTRAP" || exit $?
echo "$DEFAULT_TARGETS" | xargs "$SCRIPT_DIR/cibuild"
else
xcodebuild -list | awk -f "$SCRIPT_DIR/targets.awk" | xargs "$SCRIPT_DIR/cibuild"
fi
echo "*** The following schemes will be built:"
echo "$SCHEMES" | xargs -n 1 echo " "
echo
exit $?
fi
echo "$SCHEMES" | xargs -n 1 | (
local status=0
if [ -f "$BOOTSTRAP" ]
then
echo "*** Bootstrapping..."
bash "$BOOTSTRAP" || exit $?
fi
while read scheme
do
build_scheme "$scheme" || status=1
done
echo "*** The following targets will be built:"
exit $status
)
}
for target in "$@"
do
echo "$target"
done
find_pattern ()
echo "*** Cleaning all targets..."
xcodebuild -alltargets clean OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
run_xcodebuild ()
{
ls -d $1 2>/dev/null | head -n 1
}
local scheme=$1
run_xctool ()
{
if [ -n "$XCWORKSPACE" ]
then
xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1
elif [ -n "$XCODEPROJ" ]
then
xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1
xcodebuild -workspace "$XCWORKSPACE" -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
else
echo "*** No workspace or project file found."
exit 1
xcodebuild -scheme "$scheme" -configuration "$XCCONFIGURATION" build OBJROOT="$PWD/build" SYMROOT="$PWD/build" $XCODEBUILD_SETTINGS
fi
}
parse_build ()
{
awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null
local status=$?
return $status
}
build_scheme ()
{
local scheme=$1
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
run_xcodebuild "$scheme" 2>&1 | awk -f "$SCRIPT_DIR/xcodebuild.awk"
local awkstatus=$?
local xcstatus=${PIPESTATUS[0]}
if [ "$awkstatus" -ne "0" ]
if [ "$xcstatus" -eq "65" ]
then
# Unit tests aren't supported.
action=build
# 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
fi
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
return $xcstatus
}
export -f build_scheme
export -f run_xctool
export -f parse_build
echo "*** Building..."
main
for scheme in "$@"
do
build_scheme "$scheme" || exit $?
done
-12
View File
@@ -1,12 +0,0 @@
BEGIN {
FS = "\n";
}
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}
}
-25
View File
@@ -1,25 +0,0 @@
# 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;
}