Author SHA1 Message Date
Justin Spahr-Summers 33137bcae6 Fix update directories not being pruned (bad merge) 2014-01-24 23:09:33 -08:00
Justin Spahr-Summers 532c565b2a Need to -flattenMap: to the list of directories 2014-01-24 23:07:24 -08:00
Justin Spahr-Summers 9f5a0761ae Ignore the RACUnit we use to hack lazy memoization 2014-01-24 23:03:48 -08:00
Justin Spahr-Summers 572ef9aa9b Update RACAction usage, remove RACPromises 2014-01-24 23:01:00 -08:00
Justin Spahr-Summers b8aef91738 Merge branch 'master' into rac-3.0
Conflicts:
	External/ReactiveCocoa
	Squirrel/SQRLShipItLauncher.m
	Squirrel/SQRLUpdater.m
2014-01-24 22:48:31 -08:00
Josh Abernathy 36e0531218 Merge pull request #104 from Squirrel/dont-crash-with-no-signature
Log when we can’t get the running application code signature
2014-01-22 11:18:39 -08:00
Keith Duncan 77aec66456 Throw an exception when there’s no code signature in release 2014-01-22 11:08:31 -08:00
Keith Duncan e8523fd74d Log when we can’t get the running application code signature
Don’t crash.
2014-01-22 10:37:58 -08:00
Justin Spahr-Summers 767892c788 ++RAC to fix RACCommand KVO crashes 2014-01-21 14:36:05 -08:00
Rob Rix 34e37569d7 Merge pull request #103 from Squirrel/prune-old-updates
Prune temporary directories before performing first update check
2014-01-21 14:25:55 -08:00
Justin Spahr-Summers 4c93cb4508 Remove update directories before first check of any kind
Not just before beginning automatic updates.
2014-01-21 12:26:43 -08:00
Justin Spahr-Summers 431224c788 Test that updates are cleaned up 2014-01-21 12:16:35 -08:00
Justin Spahr-Summers ea8f805825 Remove old update directories before checking for updates 2014-01-21 11:55:52 -08:00
Justin Spahr-Summers 7bb4c131e1 Remove downloaded zips after unarchiving them 2014-01-21 11:43:32 -08:00
Justin Spahr-Summers 372a7d783a Merge pull request #101 from Squirrel/update-readme
Add steps to Readme
2014-01-20 15:57:27 -08:00
Justin Spahr-Summers b85e03fbe9 Minor ordering/phrasing tweaks 2014-01-20 15:56:31 -08:00
probablycorey 48455f1ee4 Add steps to get Squirrel to build 2014-01-20 15:47:46 -08:00
Justin Spahr-Summers 5cd5fdb2ed Merge pull request #97 from Squirrel/bridge-authorization
Don’t `__bridge` cast the `AuthorizationRef`
2014-01-07 10:49:48 -08:00
Keith Duncan aa545ed419 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	External/ReactiveCocoa
2014-01-07 18:06:35 +00:00
Keith Duncan 3d507a58a6 Check that _authorization is non NULL before freeing 2014-01-07 17:25:54 +00:00
Justin Spahr-Summers 406728c066 ++RAC 2014-01-06 12:09:29 -08:00
Keith Duncan 53adc17554 Types++ 2014-01-06 12:05:58 +00:00
Keith Duncan 0fb9ed48c9 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	Squirrel/SQRLShipItLauncher.m
2014-01-06 12:04:39 +00:00
Keith Duncan 1db648059a Wrap the authorization in an Objective-C object for lifetime management 2014-01-06 12:01:12 +00:00
Keith Duncan fccf29122d Don’t __bridge cast the AuthorizationRef 2014-01-03 20:52:33 +00:00
Matt Diephouse 502c96a6df ++RAC 2013-12-30 14:04:02 -05:00
Josh Abernathy 90a6a2fe16 Merge pull request #94 from Squirrel/update-mantle-urls
Update URLs to Mantle repository
2013-12-13 14:57:10 -08:00
Justin Spahr-Summers c2869a05e9 Update URLs to Mantle repository
This change isn’t strictly necessary due to automatic redirection, but
let’s keep our links fresh.
2013-12-13 12:55:10 -08:00
Justin Spahr-Summers aa69586334 Merge pull request #90 from Squirrel/note-rac-dependency
Note that we're dependent on ReactiveCocoa and Mantle
2013-12-06 10:03:12 -08:00
Keith Duncan 8f3f5a5a72 Revert change to ReactiveCocoa pin 2013-12-06 12:53:55 +00:00
Keith Duncan f48ccd2a21 Float the version number dependency
Saves updating this when Squirrel switches to 3.0 too
2013-12-06 12:44:45 +00:00
Keith Duncan e21fd640b9 Commas-- 2013-12-05 14:14:01 +00:00
Keith Duncan 7bb7799b17 Change version dependency to 2.0 2013-12-05 14:13:33 +00:00
Keith Duncan 13a35d195b Clarify that the host application must embed the dependencies. 2013-12-04 12:28:13 +00:00
Keith Duncan 0c95e0c8c4 Note that we're dependent on ReactiveCocoa and Mantle
The client is responsible for including these because we don’t embed them.
2013-12-03 18:21:48 +00:00
Justin Spahr-Summers e5c65e530f Merge pull request #88 from Squirrel/skip-install
SKIP_INSTALL for ShipIt
2013-11-27 10:25:49 -08:00
Keith Duncan 65f191ce30 SKIP_INSTALL for ShipIt 2013-11-27 12:34:13 +00:00
40 changed files with 437 additions and 2021 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/github/Mantle.git
url = https://github.com/MantleFramework/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
+25 -2
View File
@@ -23,13 +23,36 @@ updates installed are valid.
# Adopting Squirrel
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Run `script/bootstrap` from within the submodule
1. Add a reference to Squirrel.xcodeproj to your project
1. Add Squirrel.framework as a target dependency
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
it into your Frameworks directory
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
does not embed them itself.
# Dependencies
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
and [Mantle](https://github.com/MantleFramework/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
includes the directory that Squirrel.framework, ReactiveCocoa.framework
and Mantle.framework are copied into.
# Configuration
Once Squirrel is added to your project, you need to configure and start it.
+22 -101
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 */; };
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.h in Headers */ = {isa = PBXBuildFile; fileRef = 5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */; };
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */ = {isa = PBXBuildFile; fileRef = 5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */; };
5395C0E317E9D013001648E8 /* SQRLUpdateSpec.m in Sources */ = {isa = PBXBuildFile; fileRef = 5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */; };
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 */; };
@@ -411,8 +389,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 +408,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>"; };
@@ -456,19 +421,11 @@
53710D8117D8F5C300A992DE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = text; path = README.md; sourceTree = "<group>"; };
53710D8217D8F5C300A992DE /* targets.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = targets.awk; sourceTree = "<group>"; };
53710D8317D8F5C300A992DE /* xcodebuild.awk */ = {isa = PBXFileReference; lastKnownFileType = text; path = xcodebuild.awk; sourceTree = "<group>"; };
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SQRLAuthorization.h; sourceTree = "<group>"; };
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLAuthorization.m; sourceTree = "<group>"; };
5395C0E217E9D013001648E8 /* SQRLUpdateSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SQRLUpdateSpec.m; sourceTree = "<group>"; };
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>"; };
@@ -593,7 +550,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 */,
@@ -670,6 +626,15 @@
path = script;
sourceTree = "<group>";
};
5374DCD8187AD0DC006B7056 /* Authorization */ = {
isa = PBXGroup;
children = (
5374DCC4187AD0D8006B7056 /* SQRLAuthorization.h */,
5374DCC5187AD0D8006B7056 /* SQRLAuthorization.m */,
);
name = Authorization;
sourceTree = "<group>";
};
53B70015181E944F000FFBD0 /* Products */ = {
isa = PBXGroup;
children = (
@@ -680,29 +645,11 @@
name = Products;
sourceTree = "<group>";
};
53C3185A1816C3730074B481 /* Downloads */ = {
isa = PBXGroup;
children = (
53C318691816C38A0074B481 /* SQRLDownloadManager.h */,
53C3186A1816C38A0074B481 /* SQRLDownloadManager.m */,
536CD4E0183E5D7F000C0FD1 /* SQRLDownload.h */,
536CD4E1183E5D7F000C0FD1 /* SQRLDownload.m */,
5316AFA6188F002100C070DF /* SQRLDownload+Private.h */,
53C3186D1816C38A0074B481 /* SQRLResumableDownload.h */,
53C3186E1816C38A0074B481 /* SQRLResumableDownload.m */,
53C3186B1816C38A0074B481 /* SQRLURLConnection.h */,
53C3186C1816C38A0074B481 /* SQRLURLConnection.m */,
);
name = Downloads;
sourceTree = "<group>";
};
D00F5B8217E82CFB009A4818 /* Extensions */ = {
isa = PBXGroup;
children = (
D00F5B8917E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h */,
D00F5B8A17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m */,
5316AFAA188F0A8E00C070DF /* NSData+SQRLExtensions.h */,
5316AFAB188F0A8E00C070DF /* NSData+SQRLExtensions.m */,
);
name = Extensions;
sourceTree = "<group>";
@@ -737,10 +684,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 +706,7 @@
534FF36217D8E90A0020A51A /* TestService */,
D08D4E0F17B451500012B22D /* Supporting Files */,
);
name = TestApplication;
path = ../TestApplication;
path = TestApplication;
sourceTree = "<group>";
};
D08D4E0F17B451500012B22D /* Supporting Files */ = {
@@ -851,6 +793,7 @@
D0EDBE9F17B0C6930058BC3C /* Shared */,
D0C22BE0179CC00E00158214 /* Squirrel */,
D0C22BF6179CC00E00158214 /* SquirrelTests */,
D08D4E0E17B451500012B22D /* TestApplication */,
D014AC0317B97885007D79D0 /* ShipIt */,
D0C22BD9179CC00E00158214 /* Frameworks */,
53710D7C17D8F5C300A992DE /* Scripts */,
@@ -918,7 +861,6 @@
D0C22BF6179CC00E00158214 /* SquirrelTests */ = {
isa = PBXGroup;
children = (
D08D4E0E17B451500012B22D /* TestApplication */,
D0C22C4E179CC18000158214 /* Specs */,
D0C22BF7179CC00E00158214 /* Supporting Files */,
);
@@ -930,8 +872,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 +949,6 @@
isa = PBXGroup;
children = (
D0964B2817F2DEE700D88BF7 /* Updates */,
53C3185A1816C3730074B481 /* Downloads */,
D0964B3417F2DEF500D88BF7 /* Archives */,
D0964B3517F2DF3100D88BF7 /* IPC */,
);
@@ -1020,7 +959,6 @@
isa = PBXGroup;
children = (
D00F5B8D17E82DE6009A4818 /* NSProcessInfoExtensionsSpec.m */,
53390949184F6322009FA41F /* NSHTTPURLResponseExtensionsSpec.m */,
D0EDBEE017B0E3650058BC3C /* SQRLCodeSignatureVerifierSpec.m */,
53710D7317D8F59700A992DE /* SQRLDeepCodesignSpec.m */,
D049059A180554BA004E683E /* SQRLDirectoryManagerSpec.m */,
@@ -1032,9 +970,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 +981,7 @@
D0A56BF61804BABB00A84EDC /* Code Signing */,
D0A56BE71804BA2400A84EDC /* State */,
D06B58BC18032BE800656D97 /* Transactions */,
5374DCD8187AD0DC006B7056 /* Authorization */,
);
name = Shared;
path = Squirrel;
@@ -1094,19 +1030,12 @@
isa = PBXHeadersBuildPhase;
buildActionMask = 2147483647;
files = (
53C318711816C38A0074B481 /* SQRLURLConnection.h in Headers */,
5374DCC6187AD0D8006B7056 /* SQRLAuthorization.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 */,
D00F5B8B17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.h in Headers */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1494,7 +1423,6 @@
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 */,
D014AC1A17B979AA007D79D0 /* NSError+SQRLVerbosityExtensions.m in Sources */,
@@ -1517,25 +1445,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 */,
D00F5B8C17E82D15009A4818 /* NSProcessInfo+SQRLVersionExtensions.m in Sources */,
536CD4E3183E5D7F000C0FD1 /* SQRLDownload.m in Sources */,
5374DCC7187AD0D8006B7056 /* SQRLAuthorization.m in Sources */,
D06B58B518032B1500656D97 /* RACSignal+SQRLTransactionExtensions.m in Sources */,
53390937184F6219009FA41F /* NSHTTPURLResponse+SQRLExtensions.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1545,20 +1467,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 */,
@@ -1728,6 +1645,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Debug;
};
@@ -1737,6 +1655,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Release;
};
@@ -1746,6 +1665,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Test;
};
@@ -1755,6 +1675,7 @@
buildSettings = {
OTHER_LDFLAGS = "-all_load";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
name = Profile;
};
-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
+27
View File
@@ -0,0 +1,27 @@
//
// SQRLAuthorization.h
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Wraps an AuthorizationRef.
@interface SQRLAuthorization : NSObject
// Designated initializer.
//
// authorization - Must not be NULL, the returned object assumes ownership of
// the passed authorization.
//
// Returns an object which ties the `authorization` argument to the returned
// object's lifetime.
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
@end
+28
View File
@@ -0,0 +1,28 @@
//
// SQRLAuthorization.m
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLAuthorization.h"
@implementation SQRLAuthorization
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
NSParameterAssert(authorization != NULL);
self = [self init];
if (self == nil) return nil;
_authorization = authorization;
return self;
}
- (void)dealloc {
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
}
@end
+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
+1 -1
View File
@@ -544,7 +544,7 @@ typedef struct {
return YES;
}];
return [enumerator.rac_promise start];
return enumerator.allObjects.rac_signal;
}]
flattenMap:^(NSURL *URL) {
const char *path = URL.path.fileSystemRepresentation;
-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
+8 -8
View File
@@ -13,6 +13,7 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
@@ -108,16 +109,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}]];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -127,11 +125,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, id authorization) {
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
@@ -140,7 +140,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
-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
+8 -10
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];
});
@@ -144,7 +142,7 @@ NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
+2 -2
View File
@@ -104,8 +104,8 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// 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
// After subscribing to this signal, the receiver is responsible for terminating
// the application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
+175 -102
View File
@@ -17,13 +17,9 @@
#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"
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
@@ -36,10 +32,11 @@ const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
@interface SQRLUpdater ()
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// The directory manager used for downloads and unpacks.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
@interface SQRLUpdater ()
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@@ -50,6 +47,16 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// Sends completed or error.
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
// Lazily removes outdated temporary directories (used for previous updates)
// upon first subscription.
//
// Pruning directories while an update is pending or in progress will result in
// undefined behavior.
//
// Sends each removed directory then completes, or errors, on an unspecified
// thread.
@property (nonatomic, strong, readonly) RACSignal *prunedUpdateDirectories;
// Parses an update model from downloaded data.
//
// data - JSON data representing an update manifest. This must not be nil.
@@ -69,25 +76,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.
@@ -159,20 +163,61 @@ 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);
if (_signature == nil) {
#if DEBUG
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
return nil;
#else
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
#endif
}
@weakify(self);
_prunedUpdateDirectories = [[[[[[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSFileManager *manager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:appSupportURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, appSupportURL, error);
return YES;
}];
return [[enumerator.allObjects.rac_signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
}]
doNext:^(NSURL *directoryURL) {
NSError *error = nil;
if (![manager removeItemAtURL:directoryURL error:&error]) {
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
}
}];
}]
collect]
// These operators ensure that the actual work only executes once,
// without starting it immediately.
concat:[RACSignal never]]
shareWhileActive]
take:1]
flattenMap:^(NSArray *URLs) {
return URLs.rac_signal;
}]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
_checkForUpdatesAction = [[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
flattenMap:^(NSURLRequest *request) {
@strongify(self);
return [self checkForUpdates:request];
}]
action];
@@ -181,7 +226,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
subject]
setNameWithFormat:@"%@ updates", self];
_shipItLauncher = [[[[RACSignal
_shipItLauncher = [[[[[[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
@@ -193,10 +238,15 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
// These operators ensure that the actual work only executes once,
// without starting it immediately. Super hacky.
concat:[RACSignal return:RACUnit.defaultUnit]]
concat:[RACSignal never]]
shareWhileActive]
take:1]
ignoreValues]
setNameWithFormat:@"shipItLauncher"];
return self;
}
@@ -215,7 +265,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
@strongify(self);
return [[[self.checkForUpdatesAction
deferred]
signalWithValue:nil]
ignoreValues]
catch:^(NSError *error) {
NSLog(@"Error checking for updates: %@", error);
@@ -235,38 +285,43 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:newRequest]
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
ignoreValues]
concat:[NSURLConnection rac_sendAsynchronousRequest:newRequest]]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
if ([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];
}
}
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];
}];
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
}
- (RACSignal *)updateFromJSONData:(NSData *)data {
@@ -308,21 +363,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 +384,93 @@ 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) {
return [[[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [[RACSignal
return: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 [RACSignal return:bodyData];
}]
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]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
}]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
#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:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
};
if (mkdtemp(updateDirectoryCString) == NULL) {
int code = errno;
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not create temporary directory", nil),
NSURLErrorKey: updateDirectoryTemplate
};
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
}
NSString *updateDirectoryPath = [NSFileManager.defaultManager stringWithFileSystemRepresentation:updateDirectoryCString length:strlen(updateDirectoryCString)];
return [RACSignal return:[NSURL fileURLWithPath:updateDirectoryPath isDirectory:YES]];
}]
setNameWithFormat:@"%@ -uniqueTemporaryDirectoryForUpdate", self];
}
- (RACSignal *)updateBundleMatchingCurrentApplicationInDirectory:(NSURL *)directory {
@@ -408,8 +484,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
return YES;
}];
NSURL *updateBundleURL = [[[enumerator.rac_promise
start]
NSURL *updateBundleURL = [[enumerator.allObjects.rac_signal
filter:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
@@ -511,7 +586,7 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[[[SQRLShipItState
return [[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
@@ -529,8 +604,6 @@ const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// 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];
}
@@ -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
-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
-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
+120 -149
View File
@@ -6,12 +6,11 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTestUpdate.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "SQRLDirectoryManager.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
#import "SQRLZipArchiver.h"
#import "SQRLTestUpdate.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
SpecBegin(SQRLUpdater)
@@ -53,89 +52,140 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
describe(@"updating", ^{
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should not install a corrupt update", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
it(@"should not install a corrupt update", ^{
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
writeUpdate(update);
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
NSRunningApplication *app = launchWithEnvironment(nil);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
NSURL *updateURL = [self createTestApplicationUpdate];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
writeUpdate(update);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
writeUpdate(update);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager applicationSupportURL] first];
expect(appSupportURL).notTo.beNil();
updateDirectoryURLs = [[RACSignal
defer:^{
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
if (contents == nil) return [RACSignal empty];
return contents.rac_signal;
}]
filter:^(NSURL *directoryURL) {
return [directoryURL.lastPathComponent hasPrefix:@"update."];
}];
});
it(@"should remove downloaded archives after updating", ^{
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
// Also test that this is the only update directory (any others
// should've been removed).
NSArray *directoryURLs = [updateDirectoryURLs array];
expect(directoryURLs.count).to.equal(1);
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
expect(contents).to.equal(@[]);
});
});
});
describe(@"response handling", ^{
@@ -158,7 +208,7 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
@@ -175,90 +225,11 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] 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
+1 -1
View File
@@ -9,7 +9,7 @@
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.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"];
+1 -6
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,14 +70,10 @@
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
updateCheckCount++;
return [self.updater.checkForUpdatesAction deferred];
return [self.updater.checkForUpdatesAction signalWithValue:nil];
}]
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]