238 Commits
Author SHA1 Message Date
Keith Duncan bfbcdafae4 Signal chain++ 2014-01-21 12:58:11 -08:00
Keith Duncan 8f2c9d08b4 Loop condition++ 2014-01-21 12:56:54 -08:00
Keith Duncan 409d6ef781 Dots-- 2014-01-21 12:56:00 -08:00
Keith Duncan 8fb6d0e64f Remove unused ignoreValues 2014-01-21 12:55:57 -08:00
Keith Duncan 67cc4c2e14 Remove unused ignoreValues 2014-01-21 12:55:28 -08:00
Keith Duncan feee9f7995 Add parameter assert 2014-01-21 12:55:22 -08:00
Keith Duncan 318b9a9081 Assert that request is non-nil 2014-01-21 12:49:26 -08:00
Keith Duncan 217ada5c47 Dots-- 2014-01-21 12:47:49 -08:00
Keith Duncan 819aa8f815 Remove redundant dispose 2014-01-21 12:45:08 -08:00
Keith Duncan f0f2523b5d Remove unused property 2014-01-21 12:39:34 -08:00
Keith Duncan 283f9ef559 Tidy up retrieve and download docs 2014-01-21 12:39:23 -08:00
Keith Duncan 0f32a80019 Remove error parameter 2014-01-21 12:32:16 -08:00
Keith Duncan c76b3158e7 Equality short circuit 2014-01-21 12:30:55 -08:00
Keith Duncan ca72079898 Strongly type the isEqual: argument 2014-01-21 12:29:55 -08:00
Keith Duncan 625edb1388 Pass a mutable dictionary to the write map block 2014-01-21 12:28:54 -08:00
Keith Duncan 48705686d0 Remove redundant key path maps 2014-01-21 12:27:52 -08:00
Keith Duncan 5cd1c0ecd8 Remove unused catch: 2014-01-21 12:19:24 -08:00
Keith Duncan 94c98f4e8e Move the SHA1 and base16 encoding into an NSData category 2014-01-21 12:09:04 -08:00
Keith Duncan f1c4012aa0 Refactor the property list read + error into one method 2014-01-21 12:00:48 -08:00
Keith Duncan bb370d2b42 Remove initialised variables 2014-01-21 11:54:16 -08:00
Keith Duncan eb8ef9cd48 Read the download index uncached 2014-01-21 11:52:48 -08:00
Keith Duncan dad7bdfa89 Revert "Make the download index read/write synchronous on subscription"
This reverts commit 79dab822fd.
2014-01-21 11:51:29 -08:00
Keith Duncan 92c37377ca Move documentation to the class extension 2014-01-21 11:34:52 -08:00
Keith Duncan c65597ee6f Use +defer: rather than return:] try:] 2014-01-21 11:33:23 -08:00
Keith Duncan 18c923f343 Check for NULL before releasing the queue 2014-01-21 11:29:49 -08:00
Keith Duncan 5c9702f1d4 Use background priority for reading/writing the download index 2014-01-21 11:29:08 -08:00
Keith Duncan 7850a0707b Update documentation 2014-01-21 11:28:58 -08:00
Keith Duncan 8a3bb3d896 Move the SQRLDownload initialiser into a class extension
Document the `SQRLDownload` class.
2014-01-21 11:23:58 -08:00
Keith Duncan 40cf893899 Remove extraneous tryMap 2014-01-21 11:19:06 -08:00
Keith Duncan 539c7983c8 Add a category for creating directories 2014-01-21 11:19:02 -08:00
Keith Duncan a13090740d Whitespace-- 2014-01-21 10:59:57 -08:00
Keith Duncan f82fbaf53d More signals for creating the app support URL 2014-01-21 10:58:38 -08:00
Keith Duncan ef2bc2345a Change the idempotent URL signal methods to properties
Rename the unique directory signal to include a verb.
2014-01-21 10:58:21 -08:00
Keith Duncan e74d412eb9 Use -try instead of identity mapping 2014-01-21 10:38:18 -08:00
Keith Duncan b8ece2fc80 Merge remote-tracking branch 'origin/rac-3.0' into download-updates
Conflicts:
	Squirrel/SQRLUpdater.m
2014-01-06 15:11:27 +00:00
Justin Spahr-Summers edb4aafa31 Merge pull request #93 from Squirrel/replace-then
Remove uses of -then: and -skipUntilBlock:
2013-12-23 20:58:01 -08:00
Justin Spahr-Summers de276d73a6 Remove uses of -then: and -skipUntilBlock: 2013-12-06 23:48:06 -08:00
Justin Spahr-Summers 7655bd0a90 ++RAC 2013-12-06 23:40:17 -08:00
Justin Spahr-Summers e9a5874339 Merge pull request #89 from Squirrel/rac-3.0++
More RAC 3.0 support
2013-12-06 10:04:49 -08:00
Keith Duncan 1ac9c4ddc1 Separate the capturing of the current request from the issuing 2013-12-06 12:40:47 +00:00
Keith Duncan 9907d44f40 Complete the updates subject in dealloc 2013-12-06 12:36:03 +00:00
Keith Duncan 17c7238489 Use -aggregate... rather than implement it ourselves 2013-12-05 21:17:03 +00:00
Keith Duncan b5bbdfbed0 Don’t use dot syntax for deferred 2013-12-05 12:58:21 +00:00
Keith Duncan 28079454dc Don’t eagerly start the relaunch promise 2013-12-05 12:49:49 +00:00
Keith Duncan 210e09e079 Use the action signal to connect if needed, rather than execute: 2013-12-05 12:43:43 +00:00
Keith Duncan f0161a51d9 Name the updater updates subject 2013-12-05 12:42:11 +00:00
Keith Duncan d121f4cf7f Remove temporary variable 2013-12-05 12:41:45 +00:00
Keith Duncan 02bb683cf7 Hoist the NSURLConnection signal chain out of +defer:
The defer signal now takes the updateRequest at time of subscribe and passes it to the new method.
2013-12-05 12:40:20 +00:00
Keith Duncan d64a09bb2b Fix scoped instance variable access for @strong/@weak modifications 2013-12-05 12:32:29 +00:00
Keith Duncan 5f201f833e Remove comment 2013-12-05 12:29:03 +00:00
Keith Duncan 6e1b3c9ded Complete the updates signal when the updater is dealloc’d 2013-12-05 12:28:38 +00:00
Keith Duncan 1109d19a9e Change a couple of waits
Make these asynchronous so that they timeout
2013-12-04 18:42:50 +00:00
Keith Duncan 2c567384db Support accumulating data in SQRLURLConnection 2013-12-04 18:41:18 +00:00
Keith Duncan fcc86ca470 Merge the file scheme testing 2013-12-04 17:33:14 +00:00
Keith Duncan 7af569196e Rename the downloader to SQRLURLConnection
Add retrieval support to the interface (unimplemented) and move clients to the new interface.
2013-12-04 17:26:48 +00:00
Keith Duncan 70e7436e11 Add test that the unpacked update is in a .noindex directory 2013-12-04 16:52:20 +00:00
Keith Duncan 086a23661d Broadcast the received updates
Uses the JSON serialisation of the downloaded update, this is so I can tell test application to update, then run assertions on the downloadedUpdate from the otest process.
2013-12-04 16:23:07 +00:00
Keith Duncan e1460343ff Fix signal name 2013-12-04 16:22:47 +00:00
Keith Duncan faccaf5f6f Use the global variables for the JSON key map 2013-12-04 16:22:29 +00:00
Keith Duncan 1f8d6e87cc Add JSON serialisation support to downloaded update 2013-12-04 16:22:12 +00:00
Keith Duncan a70db2793f Add Mantle and ReactiveCocoa to TestApplication’s copy phase 2013-12-04 16:21:35 +00:00
Keith Duncan cbb75b1495 Split unarchiving and downloading signals
Move the chaining of download, unpack and find into `downloadAndPrepareUpdate:`.
2013-12-04 13:40:29 +00:00
Keith Duncan df49ea7aca Give the download the suggestedFilename
This incorporates the file extension appropriate for the response `MIMEType`.
2013-12-04 13:29:47 +00:00
Keith Duncan 2d8ea97081 Change the type of the second download tuple value 2013-12-04 13:23:49 +00:00
Keith Duncan d07c2d4d9d Move response header look up into a category
Add test and change the resumable download to use it
2013-12-04 13:22:59 +00:00
Keith Duncan 89ee65156f Remove variable pre-declaration 2013-12-03 23:19:18 +00:00
Keith Duncan 58062408d7 Remove unused method 2013-12-03 23:18:09 +00:00
Keith Duncan 46d8fa7fa1 Fix docs 2013-12-03 23:18:02 +00:00
Keith Duncan 645d276fc3 Assertions++ 2013-12-03 19:04:58 +00:00
Keith Duncan 25cb739760 Move HTTP server schenanigans into a class 2013-12-03 19:03:26 +00:00
Keith Duncan 14ff8e2012 Remove :trollface: logs that I apparently missed 2013-12-03 19:02:52 +00:00
Keith Duncan 4826034831 Merge pull request #77 from Squirrel/fix-downloader-signals
Fix NSURLConnection signal ordering

Merging this to download-updates where further review can take place
2013-12-03 10:00:54 -08:00
Keith Duncan 35081231a3 Remove the delegate stubs
Cancelling the connection should prevent further delegate messages, and if necessary cancel should block for any in flight delegate messages to complete, per the documentation.

> After this method is called, the connection’s delegate no longer receives any messages for the connection.

This also changes the delegate signal method, at time of connection creation, the delegate MUST already respond to the delegate messages, implementing them subsequently by adding an implementation results in the connection not messaging us at all. This precludes the connection from being provided as an argument to the delegate signal method.
2013-12-03 17:52:14 +00:00
Keith Duncan aa1db49ca0 Remove mention of “callback scheduler” that was removed 2013-12-03 17:19:35 +00:00
Keith Duncan 173dd52f1c Don’t -concat: the signals that return the download
This was resulting in double writes, what I actually wanted was `-then:`
2013-12-03 17:18:31 +00:00
Keith Duncan b751e75e19 Use -[SQRLDownload resumeRequest] 2013-12-03 16:05:01 +00:00
Keith Duncan 789feef723 Update to RAC 3.0 API 2013-12-03 16:04:53 +00:00
Keith Duncan f4af70b715 Merge branch 'download-updates' into fix-downloader-signals
Conflicts:
	Squirrel/SQRLDownloader.m
2013-12-03 16:03:28 +00:00
Keith Duncan 22c1a8dc07 Make the resumable download a promise
We only need to fetch it once
2013-12-03 16:00:56 +00:00
Keith Duncan 79dab822fd Make the download index read/write synchronous on subscription 2013-12-03 16:00:32 +00:00
Keith Duncan 5792209feb Remove the replay and multicasts
Attempt to remove the deliverOn scheduler now that we’re synchronous
2013-12-03 15:45:22 +00:00
Keith Duncan 1075abadec Add length assertions
These will provide easier glance-able insight into the failure, than comparing streams of hexadecimal encoded binary.
2013-12-03 15:44:39 +00:00
Keith Duncan 9ac9e32509 Replace +createSignal: with +create: 2013-12-03 13:06:57 +00:00
Keith Duncan f1d818b321 Remove uses of RACSequence 2013-12-03 13:02:37 +00:00
Keith Duncan 7834599175 Fix missed open bracket in merge 2013-12-03 12:53:30 +00:00
Keith Duncan bf4355a522 Merge branch 'rac-3.0++' into fix-downloader-signals
Conflicts:
	Squirrel/SQRLUpdater.m
2013-12-03 12:52:13 +00:00
Keith Duncan 656decb128 Fix interval checks, the API is a subscription not a signal 2013-12-03 12:39:54 +00:00
Keith Duncan 4ef110e84b Update to new RACAction API 2013-12-03 12:39:27 +00:00
Keith Duncan a22ebbfdb9 Use RACAction for checkForUpdates 2013-12-03 12:34:56 +00:00
Keith Duncan 8cdf3d0666 Fix installer test with new RAC 3.0 API changes 2013-12-03 12:34:42 +00:00
Keith Duncan ff6a2e1cb7 Remove RACSequence for the log locations 2013-12-02 16:57:14 +00:00
Keith Duncan c3a41e9032 Build the tests when building ShipIt 2013-12-02 16:11:10 +00:00
Keith Duncan 27eb479de4 Remove RACSequence, multicast and replay uses 2013-12-02 16:10:03 +00:00
Keith Duncan aab4ca3efb Remove -initially: 2013-12-02 15:54:03 +00:00
Keith Duncan 6bc3b0d466 Replace the commands with methods
I’m not sure if this is a match for `RACAction` because it needs the argument from the caller/subscriber.
2013-12-02 15:45:07 +00:00
Keith Duncan ef83ed3175 Rename -finally: to -doFinished: 2013-12-02 15:09:57 +00:00
Keith Duncan c382fbce40 Remove NSEnumerator.rac_sequence 2013-12-02 15:09:22 +00:00
Keith Duncan d0498c528e ReactiveCocoa++
Pin to working ReactiveCocoa-Mac target
2013-12-02 15:09:04 +00:00
Justin Spahr-Summers cb2502510b Update SQRLShipItLauncher 2013-11-26 12:49:25 -08:00
Justin Spahr-Summers 49a8f3b624 Update SQRLZipArchiver 2013-11-26 12:48:28 -08:00
Justin Spahr-Summers 36e3b17abb Update SQRLTerminationListener 2013-11-26 12:47:55 -08:00
Justin Spahr-Summers eb49de653b Update SQRLCodeSignature 2013-11-26 12:46:53 -08:00
Justin Spahr-Summers 35e5a03130 Update RACSignal transaction extensions 2013-11-26 12:45:17 -08:00
Justin Spahr-Summers b4387c312a ++RAC to 3.0-development 2013-11-26 12:42:40 -08:00
Keith Duncan 52f9eb8f4f Remove RunUnitTests 2013-11-21 17:35:40 +00:00
Keith Duncan cfc5bbf25f Experiment with splitting the download into two types 2013-11-21 16:04:24 +00:00
Keith Duncan aab4fc5d59 Remove unused method 2013-11-20 20:24:30 +00:00
Keith Duncan ae6db20fa7 Remove RunUnitTests 2013-11-20 20:22:51 +00:00
Justin Spahr-Summers 2010092d95 Clean up properties, document -signalForDelegate… 2013-11-08 11:11:46 -08:00
Justin Spahr-Summers 0166b2fef1 Document the logic behind the connection signals 2013-11-08 11:09:30 -08:00
Justin Spahr-Summers f80f67534c Refactor SQRLDownloader to group signal logic better 2013-11-08 11:03:07 -08:00
Justin Spahr-Summers 0b677cefc1 Stub delegate methods to "fix" exceptions 2013-11-08 10:29:37 -08:00
Justin Spahr-Summers d63ff8291e Serialize all NSURLConnection callbacks on a scheduler 2013-11-08 10:29:13 -08:00
Keith Duncan 742f46ff8f Add signal names 2013-11-07 19:06:27 +00:00
Keith Duncan 1e76fd3c15 Break the giant -download method down into smaller signal methods 2013-11-07 19:02:26 +00:00
Keith Duncan 856ac9f30f Work on inner signal as a function of response version 2013-11-07 16:44:39 +00:00
Keith Duncan e4ac04cb2b +merge: version of the response + data signal
I think this fixes ordering of the two signals, but is horrible
2013-11-07 12:15:45 +00:00
Keith Duncan 4640763f1b Merge branch 'always-create-applicationsupport' into download-updates
Conflicts:
	Squirrel/SQRLDirectoryManager.m
2013-11-07 12:11:58 +00:00
Keith Duncan 3b3408d809 Remove file not used 2013-11-05 21:43:36 +00:00
Keith Duncan d93f7d0ae8 Style change 2013-11-05 21:43:00 +00:00
Keith Duncan bdb4447511 Remove un-needed properties 2013-11-05 21:41:39 +00:00
Keith Duncan 4e36d604a0 Move the initial download lookup into a separate map 2013-11-05 21:27:33 +00:00
Keith Duncan 5bfdb4683c Remove block return types 2013-11-05 21:25:58 +00:00
Keith Duncan 860c010a06 Remove base16 alphabet 2013-11-05 21:18:52 +00:00
Keith Duncan 84652b5f5c Use NSDataReadingUncached 2013-11-05 21:17:04 +00:00
Keith Duncan a8e3bf6b30 Add NSURLErrorKey to pertinent errors 2013-11-05 21:15:29 +00:00
Keith Duncan 3f3856f38b Add disposables
Remove redundant block return types
2013-11-05 21:12:05 +00:00
Keith Duncan aa6ffea547 Don’t flattenMap and return a wrapped value 2013-11-05 21:11:45 +00:00
Keith Duncan 843f8adeb0 Use +[RACSignal merge:] 2013-11-05 21:04:55 +00:00
Keith Duncan 93df075824 Fix queue name 2013-11-05 21:00:46 +00:00
Keith Duncan 4d061c2138 Use the low priority queue for reading/writing the download index 2013-11-05 21:00:19 +00:00
Keith Duncan 854a4181ff Assert the directory manager is non nil 2013-11-05 21:00:10 +00:00
Keith Duncan 2f75b436c7 Remove static globals initialised to zero 2013-11-05 20:58:38 +00:00
Keith Duncan 6109b415f3 Style changes 2013-11-05 20:50:10 +00:00
Keith Duncan 30027e933e Functional ETag lookup 2013-11-05 20:47:52 +00:00
Keith Duncan 0a198ed1da Rename resumable request method 2013-11-05 20:44:12 +00:00
Keith Duncan ab93693ae0 Move private method documentation to the class extension 2013-11-05 20:42:52 +00:00
Keith Duncan 12ea8e209a Refactor SQRLDownloader to use -liftSelector: 2013-11-05 20:41:25 +00:00
Keith Duncan 931d19bc72 Rename the operation classes to drop operation 2013-11-05 15:51:01 +00:00
Keith Duncan fc41d39585 Use bracket imports 2013-11-05 15:21:47 +00:00
Keith Duncan a28abecb4d Remove redundant #import 2013-11-05 15:20:57 +00:00
Keith Duncan 8ba490e5d3 Revise documentation for the download operation initialiser 2013-11-05 15:20:57 +00:00
Keith Duncan be824ed1fb Revise download operation documentation 2013-11-05 15:20:57 +00:00
Keith Duncan cb06bd800e Use the plural “downloads” for the directory name 2013-11-05 15:20:57 +00:00
Keith Duncan 6c18360388 Fix indentation
Replace `-flattenMap:` with `-tryMap:`
2013-11-05 15:20:57 +00:00
Keith Duncan d6356b0255 Clarify documentation 2013-11-05 15:20:56 +00:00
Keith Duncan 5e4e7d6fbc Split the directory URL mapping from the creation 2013-11-05 15:20:56 +00:00
Keith Duncan bb56dc4c87 Rename the per update method, clarify the docs 2013-11-05 15:20:52 +00:00
Keith Duncan a91faeaa6a Always create the application support directory 2013-11-05 14:32:40 +00:00
Keith Duncan e5b241dfc1 Fix indentation
Replace `-flattenMap:` with `-tryMap:`
2013-11-05 14:15:57 +00:00
Keith Duncan 17975dd64e Clarify documentation 2013-11-05 14:14:33 +00:00
Keith Duncan 33ee8ca29d Split the directory URL mapping from the creation 2013-11-05 14:11:49 +00:00
Keith Duncan a9882a73d0 Rename the per update method, clarify the docs 2013-11-05 14:11:23 +00:00
Keith Duncan 0e9ed45112 Move the unique temporary directory into SQRLDirectoryManager
This revives the use of .noindex for the unpack directories, removes `mkdtemp` and replaces with `globallyUniqueIdentifier` based directory names.
2013-10-31 17:25:21 +00:00
Keith Duncan 1e59131f03 Add test that the downloads are cleaned up after successful install 2013-10-31 16:41:05 +00:00
Keith Duncan c86dfb14c3 Disable the deep codesign test
This is failing on 10.9 and is annoying
2013-10-31 16:08:31 +00:00
Keith Duncan fcb1810178 Ignore values when removing state, we only need errors 2013-10-31 16:01:08 +00:00
Keith Duncan 0126d18c00 More +tryMap: 2013-10-31 16:00:54 +00:00
Keith Duncan 6abd2a6c82 Move no such file error catching into SQRLResumableDownloadManager 2013-10-31 15:10:17 +00:00
Keith Duncan 7532c29e8e Remove resumable downloads after one completes
The assumption is that all other pending downloads are now redundant because we have an update and have moved it out of the resumable download cache.

This will be skipped if there’s an error, and we ignore errors removing the other downloads, the caller only wants to know about errors downloading the update it wanted.
2013-10-31 14:59:02 +00:00
Keith Duncan f3c96e219c Adopt +try: and +tryMap:
Also reduces the scope of the mutual exclusion in the index reader, we just need to read the data with writers excluded, the transform using `NSKeyedUnarchiver` can be done outside this scope.
2013-10-31 14:57:48 +00:00
Keith Duncan 6a85e07b93 Add default body data
If the connection doesn’t return an error we should provide non-nil body data
2013-10-31 14:27:11 +00:00
Keith Duncan 57f339b2e4 Merge branch 'master' into download-updates
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLUpdate.m
	Squirrel/SQRLUpdater.m
	SquirrelTests/SQRLTestCase.h
2013-10-31 14:23:56 +00:00
Keith Duncan 8c303fb371 Use Mantle for SQRLResumableDownload 2013-10-24 11:10:07 +01:00
Keith Duncan b3f397b81e Use RACTupleUnpack in the tests too
Update to the new type ordering
2013-10-23 14:47:43 +01:00
Keith Duncan 10e7af2f81 Plug the URLConnection and Download operations into the updater 2013-10-23 14:42:32 +01:00
Keith Duncan a71b6f5efe Change the order of values in the download tuple
This matches the URL connection tuple order of {response, value} where value is either the response data, or the NSURL of the data location.
2013-10-23 14:41:40 +01:00
Keith Duncan df45644b6f Remove block return types where applicable 2013-10-23 14:31:21 +01:00
Keith Duncan f4492c20a5 RAC SQRLURLConnectionOperation 2013-10-23 14:26:43 +01:00
Keith Duncan 2b5b334903 Use RACTuplePack and Unpack
Use a compound disposable
2013-10-23 14:26:16 +01:00
Keith Duncan 32cccdc77a Move the connection subject to before starting the connection 2013-10-23 14:03:54 +01:00
Keith Duncan 8247064cb6 RACify the download 2013-10-23 13:56:17 +01:00
Keith Duncan c20c540e8a Use sel_getName for the signal name
Avoids duplicating the selector written out twice
2013-10-23 12:10:02 +01:00
Keith Duncan 07cb7a07c9 More RAC for the download manager
Retains the previous multi reader single writer by using a dispatch queue and `+[RACSignal createSignal:]` to bring our own concurrency rather than using multiple schedulers.
2013-10-22 20:32:21 +01:00
Keith Duncan b35b602824 More RAC in SQRLResumableDownloadManager 2013-10-22 18:29:48 +01:00
Keith Duncan f400da11f3 Change the removeAllResumableDownloads signal
Now errors or completes, no values are sent
2013-10-22 17:23:54 +01:00
Keith Duncan 14a36b50a4 Add name to the downloadStoreIndexFileLocation signal 2013-10-22 17:23:41 +01:00
Keith Duncan 98cc37d907 Only check for nil hosts on non file:// scheme URLs
file scheme URLs have a nil host on 10.9
2013-10-22 17:22:05 +01:00
Keith Duncan e0cf250d7b Fix up tests with new SQRLResumableDownloadManager interface 2013-10-22 16:46:40 +01:00
Keith Duncan cf8b4bb853 Add missed specs from the RAC merge 2013-10-22 16:22:17 +01:00
Keith Duncan 8f870954a6 Merge remote-tracking branch 'origin/master' into download-updates 2013-10-22 16:07:21 +01:00
Keith Duncan 871379d9e0 Rename SQRLDownloadController -> SQRLResumableDownloadManager 2013-10-22 16:03:12 +01:00
Keith Duncan c13c8de24d Merge branch 'bring-the-rac' into download-updates
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLDirectoryManager.h
	Squirrel/SQRLDirectoryManager.m
	Squirrel/SQRLUpdate+Private.h
	Squirrel/SQRLUpdate.h
	Squirrel/SQRLUpdate.m
	Squirrel/SQRLUpdater.h
	Squirrel/SQRLUpdater.m
	Squirrel/SQRLZipArchiver.m
	Squirrel/SQRLZipOperation.h
	SquirrelTests/SQRLUpdateSpec.m
	SquirrelTests/SQRLUpdaterSpec.m
	SquirrelTests/SQRLZipOperationSpec.m

This is the minimum-viable-merge, I haven’t RAC'd the download code yet, for instance using `first` to bridge values from the signals into the pre-existing code. Going to RAC this next.
2013-10-22 15:55:33 +01:00
Keith Duncan 01f4acfe5c Improve SQRLResumableDownload docs 2013-10-16 18:13:12 +01:00
Keith Duncan 0f9bd718a8 Add test for the download not being resumed if the ETag had changed 2013-10-09 16:57:24 +01:00
Keith Duncan 58b8376f54 Smarter server which uses a block for response->request mapping
Add expects for the requests to not have / have If-Range and Range headers
2013-10-09 16:37:16 +01:00
Keith Duncan d92516dee4 Add in process HTTP server to respond to requests
This is to test the download operation, and is very dumb at the moment, going to make it smarter in terms of responding to Range requests
2013-10-09 16:17:30 +01:00
Keith Duncan 6f3ba37b19 Fix wrong message to add the Range header 2013-10-09 16:17:18 +01:00
Keith Duncan 69c05bf159 Make addCleanupBlock: public 2013-10-09 16:16:41 +01:00
Keith Duncan b35b11a634 Add test for resumable downloads 2013-10-08 17:27:52 +01:00
Keith Duncan 07cd9c19a7 Initialise operations with a download controller 2013-10-08 17:26:28 +01:00
Keith Duncan e5a9c2c740 Replace update operation completionProvider block 2013-10-04 18:00:32 +01:00
Keith Duncan 1072ef3c16 Replace zip operation completion provider block 2013-10-04 17:59:13 +01:00
Keith Duncan 7a23627711 Replace download operation completionProvider block 2013-10-04 17:56:54 +01:00
Keith Duncan 2713404d29 Make SQRLURLConnectionOperation response provider a method
This is still a block internally so that the completion is atomically a failure or the entire response body.
2013-10-04 17:35:09 +01:00
Keith Duncan d11f29346a Add return statements when isCancelled 2013-10-04 17:19:30 +01:00
Keith Duncan f5e7c52ba8 Add missing default completion provider 2013-10-04 17:07:59 +01:00
Keith Duncan 704933eaa1 Rename the SQRLDirectoryManager methods 2013-10-04 16:58:48 +01:00
Keith Duncan db416ac0f0 Collapse return lines 2013-10-04 16:52:56 +01:00
Keith Duncan f2f3f9922a Replace block enumeration with fast enumeration 2013-10-04 16:52:03 +01:00
Keith Duncan 4f114dc343 Loop when writing data to the output stream
Handle {POSIX,EINTR} errors otherwise abort with an error
2013-10-04 16:48:15 +01:00
Keith Duncan bf64476b13 Move [connection cancel] into finish 2013-10-04 15:44:38 +01:00
Keith Duncan b31f881103 Remove duplicate [super cancel] 2013-10-04 15:40:11 +01:00
Keith Duncan 665b947a1e Tweak queue name 2013-10-01 12:01:39 +01:00
Keith Duncan 36b5feec15 Secure decoding-- 2013-09-30 23:19:07 +01:00
Keith Duncan 85309ede82 More removing of the typedef 2013-09-30 23:14:26 +01:00
Keith Duncan d9e37ab259 Fix removal of typedef, building++ 2013-09-30 23:11:12 +01:00
Keith Duncan fed28478e4 Remove block typedef 2013-09-30 23:06:28 +01:00
Keith Duncan 4b9c02563f Docs-- 2013-09-30 23:06:22 +01:00
Keith Duncan 94837037f6 Add to the comment on why we use .noindex 2013-09-30 22:59:20 +01:00
Keith Duncan d54b0c2bfa Docs++ 2013-09-30 22:48:02 +01:00
Keith Duncan 21bd5012d6 Don’t implicitly create directories
This makes error reporting hard, instead document that the directories are not created and have callers create them where the error can be reported
2013-09-30 22:46:53 +01:00
Keith Duncan 4ffe08f9c6 Prefer NSBundle.mainBundle for the current host
Only use `NSRunningApplication` when it fails which it does in otest
2013-09-30 15:46:25 +01:00
Keith Duncan a314caa355 Assert appIdentifier isn’t nil 2013-09-30 15:39:56 +01:00
Keith Duncan 6819ce4215 Rename SQRLFileManager to SQRLDirectoryManager 2013-09-30 15:38:55 +01:00
Keith Duncan efba16e355 self.class++ 2013-09-30 15:14:47 +01:00
Keith Duncan af8694a8f6 Change the downloadController interface to take requests 2013-09-30 15:13:27 +01:00
Keith Duncan 4c562a7c2e Rename the queues to use Class.property naming 2013-09-30 15:06:58 +01:00
Keith Duncan f2c534ff30 Change the operation state properties to atomic 2013-09-30 14:50:59 +01:00
Keith Duncan 937319e9f5 Remove unused variable 2013-09-30 14:46:38 +01:00
Keith Duncan ef28defe9c Fix some missing error byref and one success path case 2013-09-30 14:35:07 +01:00
Keith Duncan 1561118861 Replace file coordination with a dispatch queue
Use a barrier block on a concurrent queue to preserve multireader-singlewriter from the file coordination API
2013-09-30 14:14:40 +01:00
Keith Duncan 755d50263d Move Index.plist into the per-bundle directory out of downloads 2013-09-30 13:53:06 +01:00
Keith Duncan 97ecb1bf72 Fix leading zero when base16ing data 2013-09-30 13:41:33 +01:00
Keith Duncan b09a0cb3c8 Make download remove method error byref
Rather than logging allow callers to handle the error themselves, the tests simply log the error
2013-09-30 13:25:48 +01:00
Keith Duncan 8f9bd34e2b Imports++ 2013-09-30 13:22:34 +01:00
Keith Duncan 3b2e3639d1 Docs++ 2013-09-30 13:22:18 +01:00
Keith Duncan dc861aba42 Add SQRLResumableDownload model class 2013-09-30 13:15:59 +01:00
Keith Duncan a12d5d5cda Store the full NSHTTPURLResponse in the download 2013-09-30 12:40:41 +01:00
Keith Duncan c4144ca55d Make SQRLDownloadController methods instance methods
Add default instance class method, doesn’t initialise with any parameters currently so it just returns a new instance each time
2013-09-30 12:25:08 +01:00
Keith Duncan a2dfe45177 More docs 2013-09-27 18:54:29 +01:00
Keith Duncan 7a667b8fc4 Docs++ 2013-09-27 18:48:48 +01:00
Keith Duncan c516873458 Add SQRLUpdateOperation which replaces the state in SQRLUpdater
This operation creates the others to partition the work
2013-09-27 18:38:16 +01:00
Keith Duncan 6761505219 New SQRLUpdate initialisation method, supports errors byref
Move the root JSON type check into the update initialiser too
Add downloaded update property for the unpacked update ready to install
2013-09-27 17:39:21 +01:00
Keith Duncan f3b4ba98a9 Add SQRLURLConnectionOperation
This operation provides `NSOperation` dependencies to `NSURLConnection` and accumulates the response in memory
2013-09-27 17:32:52 +01:00
Keith Duncan c194810749 Add SQRLDownloadController and SRQLDownloadOperation
These classes enable resumable downloads by tracking the ETag for a response file, and issuing the requests with an `If-Range` header

This only tests downloading from file:// scheme URLs, I’d like to add an in process HTTP server which disconnects part of the way through the download first time, we download a second time and then test that it resumed.
2013-09-27 17:29:26 +01:00
Keith Duncan 6702f9515d Add SQRLFileManager
For generating URLs for the directories the downloads and pending updates will be stored in
2013-09-27 17:03:37 +01:00
Keith Duncan 39cf57582f Replace the zip archiver with an operation and a completionProvider
Rather than waiting for the completionBlock to be invoked, add a dependency of the operation and invoke the completionProvider to get the result
2013-09-27 17:01:05 +01:00
92 changed files with 4152 additions and 2852 deletions
+6 -19
View File
@@ -1,22 +1,9 @@
[submodule "External/ReactiveCocoa"]
path = External/ReactiveCocoa
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/github/Mantle.git
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
[submodule "Carthage.checkout/Nimble"]
path = Carthage/Checkouts/Nimble
url = https://github.com/Quick/Nimble.git
[submodule "Carthage.checkout/ReactiveCocoa"]
path = Carthage/Checkouts/ReactiveCocoa
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "Carthage.checkout/Quick"]
path = Carthage/Checkouts/Quick
url = https://github.com/Quick/Quick.git
[submodule "Carthage.checkout/xcconfigs"]
path = Carthage/Checkouts/xcconfigs
url = https://github.com/jspahrsummers/xcconfigs.git
[submodule "Carthage.checkout/Mantle"]
path = Carthage/Checkouts/Mantle
url = https://github.com/Mantle/Mantle.git
[submodule "Carthage/Checkouts/Nimble"]
url = https://github.com/Quick/Nimble.git
[submodule "Carthage/Checkouts/Quick"]
url = https://github.com/Quick/Quick.git
-2
View File
@@ -1,2 +0,0 @@
github "Mantle/Mantle" ~> 1.5.3
github "ReactiveCocoa/ReactiveCocoa" ~> 2.4.3
-3
View File
@@ -1,3 +0,0 @@
github "jspahrsummers/xcconfigs" >= 0.7.1
github "Quick/Quick" ~> 0.2
github "Quick/Nimble" ~> 0.2
-5
View File
@@ -1,5 +0,0 @@
github "Mantle/Mantle" "1.5.3"
github "Quick/Nimble" "v0.2.0"
github "Quick/Quick" "v0.2.2"
github "ReactiveCocoa/ReactiveCocoa" "v2.4.3"
github "jspahrsummers/xcconfigs" "0.7.1"
Vendored Submodule
+1
Submodule External/Mantle added at 8aef509d18
Vendored Submodule
+1
Submodule External/ReactiveCocoa added at 0138dabe60
+31 -94
View File
@@ -13,49 +13,23 @@ suitable update.
The update JSON Squirrel requests should be dynamically generated based on
criteria in the request, and whether an update is required. Squirrel relies
on server side support for determining whether an update is required, see
[Server Support](#server-support).
on server side support for determining whether an update is required, see [Server
Support](#server-support) below.
Squirrel's installer is also designed to be fault tolerant, and ensure that any
Squirrel’s installer is also designed to be fault tolerant, and ensure that any
updates installed are valid.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
# Adopting Squirrel
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Run `script/bootstrap` from within the submodule
1. Add references to Squirrel.xcodeproj and its [dependencies](#dependencies) to
your project
1. Add a reference to Squirrel.xcodeproj to your project
1. Add Squirrel.framework as a target dependency
1. Link Squirrel.framework and add it to a Copy Files build phase which copies
it into your Frameworks directory
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
does not embed them itself.
If you’re developing Squirrel on its own, then use `Squirrel.xcworkspace`.
# Dependencies
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
and [Mantle](https://github.com/Mantle/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
includes the directory that Squirrel.framework, ReactiveCocoa.framework
and Mantle.framework are copied into.
# Configuration
1. Ensure your Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
Once Squirrel is added to your project, you need to configure and start it.
@@ -63,16 +37,7 @@ Once Squirrel is added to your project, you need to configure and start it.
#import <Squirrel/Squirrel.h>
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
NSURLComponents *components = [[NSURLComponents alloc] init];
components.scheme = @"http";
components.host = @"mycompany.com";
components.path = @"/myapp/latest";
NSString *bundleVersion = NSBundle.mainBundle.sqrl_bundleVersion;
components.query = [[NSString stringWithFormat:@"version=%@", bundleVersion] stringByAddingPercentEncodingWithAllowedCharacters:NSCharacterSet.URLQueryAllowedCharacterSet]
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:components.URL]];
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://mycompany.com/myapp/latest"]]];
// Check for updates every 4 hours.
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
@@ -83,26 +48,7 @@ Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
## Update Requests
Squirrel is indifferent to the request the client application provides for
update checking. `Accept: application/json` is added to the request headers
because Squirrel is responsible for parsing the response.
For the requirements imposed on the responses and the body format of an update
response see [Server Support](#server-support).
Your update request must *at least* include a version identifier so that the
server can determine whether an update for this specific version is required. It
may also include other identifying criteria such as operating system version or
username, to allow the server to deliver as fine grained an update as you
would like.
How you include the version identifier or other criteria is specific to the
server that you are requesting updates from. A common approach is to use query
parameters, [Configuration](#configuration) shows an example of this.
## Update Available Notifications
## Update Notifications
To know when an update is ready to be installed, you can subscribe to the
`updates` signal on `SQRLUpdater`:
@@ -113,18 +59,12 @@ To know when an update is ready to be installed, you can subscribe to the
}];
```
## Installing Updates
If you've been notified of an available update, and don't want to wait for it to
be installed automatically, you can terminate the app to begin the installation
process immediately.
While downloaded updates are automatically installed when your application
terminates, if don't want to wait you can manually terminate the app to begin
the installation process immediately.
Once an [update available notification](#update-available-notifications) has
been received, you may want to present an interface informing the user about
the update and offering the ability to install and relaunch.
To explicitly install a downloaded update and automatically relaunch afterward,
subscribe to the `relaunchToInstallUpdate` signal on `SQRLUpdater`:
If you want to install a downloaded update and automatically relaunch afterward,
`SQRLUpdater` can do that:
```objc
[[self.updater relaunchToInstallUpdate] subscribeError:^(NSError *error) {
@@ -132,26 +72,10 @@ subscribe to the `relaunchToInstallUpdate` signal on `SQRLUpdater`:
}];
```
# Server Support
# Update JSON Format
Your server should determine whether an update is required based on the
[Update Request](#update-requests) your client issues.
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
[update JSON](#update-json-format) in the body. Squirrel **will** download and
install this update, even if the version of the update is the same as the
currently running version. To save redundantly downloading the same version
multiple times your server must not inform the client to update.
If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
## Update JSON Format
When an update is available, Squirrel expects the following schema in response
to the update request provided:
Squirrel requests the URL you provide with `Accept: application/json` and
expects the following schema in response:
```json
{
@@ -169,10 +93,23 @@ installing ZIP updates. If future update formats are supported their MIME type
will be added to the `Accept` header so that your server can return the
appropriate format.
"pub_date" if present must be formatted according to ISO 8601.
"pub_date" if present must be formatted according to ISO 8601
# Server Support
If an update is required your server should respond with a status code of
[200 OK](http://tools.ietf.org/html/rfc2616#section-10.2.1) and include the
update JSON in the body. Squirrel **will** download and install this update,
even if the version of the update is the same as the currently running version.
To save redundantly downloading the same version multiple times your server must
inform the client not to update.
If no update is required your server must respond with a status code of
[204 No Content](http://tools.ietf.org/html/rfc2616#section-10.2.5). Squirrel
will check for an update again at the interval you specify.
# User Interface
Squirrel does not provide any GUI components for presenting updates. If you want
to indicate updates to the user, make sure to [listen for downloaded
updates](#update-available-notifications).
updates](#update-notifications).
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0620"
version = "2.0">
LastUpgradeVersion = "0500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -22,14 +22,14 @@
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.xctest"
BuildableName = "SquirrelTests.octest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
@@ -39,56 +39,30 @@
<TestAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "NO"
buildConfiguration = "Debug">
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.xctest"
BuildableName = "SquirrelTests.octest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "FORCE_APP_IDENTIFIER"
value = "com.github.Squirrel.SquirrelTests"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugXPCServices = "NO"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQRLUpdateFromURL"
@@ -105,15 +79,6 @@
useCustomWorkingDirectory = "NO"
buildConfiguration = "Profile"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0620"
LastUpgradeVersion = "0500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -48,8 +48,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -67,8 +66,7 @@
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableProductRunnable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0620"
LastUpgradeVersion = "0500"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -20,6 +20,20 @@
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.octest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@@ -39,15 +53,6 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
-22
View File
@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Squirrel.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Mantle/Mantle.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/ReactiveCocoa/ReactiveCocoa.xcodeproj">
</FileRef>
<FileRef
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Quick/Quick.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
</FileRef>
</Workspace>
+19
View File
@@ -0,0 +1,19 @@
//
// NSData+SQRLExtensions.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSData (SQRLExtensions)
// Returns an NSData for the SHA1 hash of the receiver.
- (NSData *)sqrl_SHA1Hash;
// Returns the base16 encoding of the receiver.
- (NSString *)sqrl_base16String;
@end
+31
View File
@@ -0,0 +1,31 @@
//
// NSData+SQRLExtensions.m
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "NSData+SQRLExtensions.h"
#import <CommonCrypto/CommonCrypto.h>
@implementation NSData (SQRLExtensions)
- (NSData *)sqrl_SHA1Hash {
unsigned char hash[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(self.bytes, (CC_LONG)self.length, hash);
return [NSData dataWithBytes:hash length:sizeof(hash) / sizeof(*hash)];
}
- (NSString *)sqrl_base16String {
NSMutableString *base16 = [NSMutableString stringWithCapacity:self.length * 2];
uint8_t *bytes = (uint8_t *)self.bytes;
for (NSUInteger idx = 0; idx < self.length; idx++) {
uint8_t byte = *(bytes + idx);
[base16 appendFormat:@"%02hhx", byte];
}
return base16;
}
@end
@@ -0,0 +1,18 @@
//
// NSHTTPURLResponse+SQRLExtensions.h
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSHTTPURLResponse (SQRLExtensions)
// Case insensitively look up a header field value.
//
// Returns the value of the header if present or nil otherwise.
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field;
@end
@@ -0,0 +1,26 @@
//
// NSHTTPURLResponse+SQRLExtensions.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "NSHTTPURLResponse+SQRLExtensions.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@implementation NSHTTPURLResponse (SQRLExtensions)
- (NSString *)sqrl_valueForHTTPHeaderField:(NSString *)field {
return [[[self.allHeaderFields.rac_signal
filter:^ BOOL (RACTuple *keyValuePair) {
return [keyValuePair[0] caseInsensitiveCompare:field] == NSOrderedSame;
}]
reduceEach:^(NSString *key, NSString *value) {
return value;
}]
first];
}
@end
@@ -0,0 +1,15 @@
//
// RACSignal+SQRLFileManagerExtensions.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface RACSignal (SQRLFileManagerExtensions)
- (RACSignal *)sqrl_tryCreateDirectory;
@end
@@ -0,0 +1,21 @@
//
// RACSignal+SQRLFileManagerExtensions.m
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "RACSignal+SQRLFileManagerExtensions.h"
@implementation RACSignal (SQRLFileManagerExtensions)
- (RACSignal *)sqrl_tryCreateDirectory {
return [[self
try:^(NSURL *directoryURL, NSError **errorRef) {
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:nil error:errorRef];
}]
setNameWithFormat:@"%@ -sqrl_tryCreateDirectory", self];
}
@end
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <ReactiveCocoa/RACSignal.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface RACSignal (SQRLTransactionExtensions)
+13 -18
View File
@@ -7,8 +7,6 @@
//
#import "RACSignal+SQRLTransactionExtensions.h"
#import <ReactiveCocoa/RACDisposable.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
// How long before power assertions time out.
@@ -99,15 +97,12 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
return [RACDisposable disposableWithBlock:^{
[subscriptionDisposable dispose];
[transactionDisposable dispose];
}];
}] setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
[subscriber.disposable addDisposable:SQRLCreateTransaction(name, description)];
[self subscribe:subscriber];
}]
setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
}
- (RACSignal *)sqrl_addSubscriptionTransactionWithName:(NSString *)name description:(NSString *)descriptionFormat, ... {
@@ -119,13 +114,13 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
[transactionDisposable dispose];
return subscriptionDisposable;
}] setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
[self subscribe:subscriber];
[transactionDisposable dispose];
}]
setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
}
@end
-27
View File
@@ -1,27 +0,0 @@
//
// SQRLAuthorization.h
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Wraps an AuthorizationRef.
@interface SQRLAuthorization : NSObject
// Designated initializer.
//
// authorization - Must not be NULL, the returned object assumes ownership of
// the passed authorization.
//
// Returns an object which ties the `authorization` argument to the returned
// object's lifetime.
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
@end
-28
View File
@@ -1,28 +0,0 @@
//
// SQRLAuthorization.m
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLAuthorization.h"
@implementation SQRLAuthorization
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
NSParameterAssert(authorization != NULL);
self = [self init];
if (self == nil) return nil;
_authorization = authorization;
return self;
}
- (void)dealloc {
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
}
@end
+8 -20
View File
@@ -21,44 +21,32 @@ extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
@class RACSignal;
// Implements the verification of Apple code signatures and requirements.
// Implements the verification of Apple code signatures.
@interface SQRLCodeSignature : MTLModel
// A serialized version of the `SecRequirementRef` that the receiver was
// initialized with.
@property (nonatomic, copy, readonly) NSData *requirementData;
// Determines the designated requirement of the currently-executing application.
// The returned code signature can be used to verify that a bundle is code sign
// valid and meets the designated requirement of the current application.
// Determines the code signature of the currently-executing application.
//
// error - If not NULL, set to any error that occurs.
//
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
// designated requirement for the running code.
// signature for the running code.
+ (instancetype)currentApplicationSignature:(NSError **)error;
// Determines the designated requirement of the specified bundle. The returned
// code signature can be used to verify that an arbitrary bundle is valid and
// meets the designated requirement of `bundle`.
// Initializes the receiver with the given requirement.
//
// bundleURL - Must not be nil, the location of a bundle directory structure
// which has been code signed and includes a designated requirement.
// This bundle's designated requirement is used when verifying other
// bundles.
// error - If not NULL, set to any error that occurs.
//
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
// designated requirement of the bundle at `bundleURL`.
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)error;
// requirement - The code requirement for tested bundles. This must not be NULL.
- (id)initWithRequirement:(SecRequirementRef)requirement;
// Verifies the code signature of the specified bundle and verifies that the
// bundle meets the receiver's requirement.
// Verifies that the code signature of the specified bundle matches the receiver.
//
// bundleURL - The URL to the bundle to verify on disk. This must not be nil.
//
// Returns a signal which will synchronously send completed on success, or error
// if the requirement was not verified successfully.
// if the code signature was not verified successfully.
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL;
@end
+40 -59
View File
@@ -7,11 +7,8 @@
//
#import "SQRLCodeSignature.h"
#import "EXTKeyPathCoding.h"
#import "EXTScope.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
@@ -22,14 +19,9 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
@interface SQRLCodeSignature ()
// A `SecRequirementRef` that tested bundles must satisfy.
@property (atomic, strong) id requirement;
// Initializes the receiver with the given requirement.
//
// This is the designated initializer for this class.
//
// requirement - The code requirement for tested bundles. This must not be NULL.
- (id)initWithRequirement:(SecRequirementRef)requirement;
// This property is automatically retained.
@property (atomic) id requirement;
@end
@@ -45,49 +37,8 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
#pragma mark Lifecycle
+ (instancetype)currentApplicationSignature:(NSError **)errorRef {
SecCodeRef staticCode = NULL;
OSStatus error = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
if (error != noErr) {
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
return nil;
}
@onExit {
CFRelease(staticCode);
};
return [self signatureWithCode:staticCode error:errorRef];
}
+ (instancetype)signatureWithBundle:(NSURL *)bundleURL error:(NSError **)errorRef {
SecStaticCodeRef bundleCode = NULL;
OSStatus error = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &bundleCode);
if (error != noErr) {
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
return nil;
}
@onExit {
CFRelease(bundleCode);
};
return [self signatureWithCode:bundleCode error:errorRef];
}
+ (instancetype)signatureWithCode:(SecStaticCodeRef)code error:(NSError **)errorRef {
SecRequirementRef designatedRequirement = NULL;
OSStatus error = SecCodeCopyDesignatedRequirement(code, kSecCSDefaultFlags, &designatedRequirement);
if (error != noErr) {
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSOSStatusErrorDomain code:error userInfo:nil];
return nil;
}
@onExit {
CFRelease(designatedRequirement);
};
return [[SQRLCodeSignature alloc] initWithRequirement:designatedRequirement];
+ (instancetype)currentApplicationSignature:(NSError **)error {
return [self modelWithDictionary:nil error:error];
}
- (id)initWithRequirement:(SecRequirementRef)requirement {
@@ -98,12 +49,41 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
} error:NULL];
}
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [super initWithDictionary:dictionary error:error];
if (self == nil) return nil;
if (self.requirement == nil) {
SecCodeRef staticCode = NULL;
OSStatus result = SecCodeCopySelf(kSecCSDefaultFlags, &staticCode);
@onExit {
if (staticCode != NULL) CFRelease(staticCode);
};
if (result != noErr) {
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
return nil;
}
SecRequirementRef req = NULL;
result = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &req);
self.requirement = CFBridgingRelease(req);
if (result != noErr) {
if (error != NULL) *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:result userInfo:nil];
return nil;
}
}
return self;
}
#pragma mark Verification
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
return [[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
return [[RACSignal create:^(id<RACSubscriber> subscriber) {
SecStaticCodeRef staticCode = NULL;
OSStatus result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
@@ -120,8 +100,10 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return nil;
return;
}
if (subscriber.disposable.disposed) return;
CFErrorRef validityError = NULL;
result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, (__bridge SecRequirementRef)self.requirement, &validityError);
@@ -139,11 +121,10 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return nil;
return;
}
[subscriber sendCompleted];
return nil;
}] setNameWithFormat:@"-verifyCodeSignatureOfBundle: %@", bundleURL];
}
+17 -7
View File
@@ -6,16 +6,13 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
@class RACSignal;
// Provides the file locations that Squirrel/ShipIt use.
@interface SQRLDirectoryManager : NSObject
// The application identifier to use in file locations.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
// Returns the shared `SQRLDirectoryManager` for the running application, based
// on the bundle identifier or application name.
+ (instancetype)currentApplicationManager;
@@ -29,14 +26,27 @@
// on-disk locations for. This must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
// Finds or creates an Application Support folder.
// Finds or creates an Application Support subdirectory for the receiver’s
// application identifier.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)applicationSupportURL;
@property (readonly, nonatomic) RACSignal *applicationSupportURL;
// Finds or creates a downloads directory to store update downloads in prior to
// installation. This is an applicationSupportURL subdirectory.
//
// Returns a signal which sends a URL then completes, or errors.
@property (readonly, nonatomic) RACSignal *downloadDirectoryURL;
// Creates a new directory to unpack a downloaded update into prior to
// installation. This is an applicationSupportURL subdirectory.
//
// Returns a signal which sends a URL then completes, or errors.
- (RACSignal *)createUniqueUpdateDirectoryURL;
// Determines where archived `SQRLShipItState` should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStateURL;
@property (readonly, nonatomic) RACSignal *shipItStateURL;
@end
+59 -11
View File
@@ -7,8 +7,16 @@
//
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import "RACSignal+SQRLFileManagerExtensions.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
@interface SQRLDirectoryManager ()
// The application identifier to use in file locations.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
@end
@implementation SQRLDirectoryManager
@@ -24,7 +32,7 @@
// Should only fallback to when running under otest, where
// NSBundle.mainBundle doesn't return useful data.
if (identifier == nil) {
identifier = NSProcessInfo.processInfo.environment[@"FORCE_APP_IDENTIFIER"];
identifier = NSRunningApplication.currentApplication.localizedName;
}
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
@@ -47,25 +55,65 @@
#pragma mark Folder URLs
+ (NSString *)fileSystemNameForIdentifier:(NSString *)identifier {
// Periods are problematic in the filesystem because they denote file type.
// A directory can become a package which can be undesirable.
return [identifier stringByReplacingOccurrencesOfString:@"." withString:@"~"];
}
- (RACSignal *)applicationSupportURL {
return [[RACSignal
return [[[[RACSignal
defer:^{
NSError *error = nil;
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (folderURL == nil) {
NSURL *directoryURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (directoryURL == nil) {
return [RACSignal error:error];
}
folderURL = [folderURL URLByAppendingPathComponent:self.applicationIdentifier];
if (![NSFileManager.defaultManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:&error]) {
return [RACSignal error:error];
}
return [RACSignal return:folderURL];
return [RACSignal return:directoryURL];
}]
map:^ (NSURL *directoryURL) {
RACSignal *identifierComponents = [@[ self.applicationIdentifier, @"com.github.Squirrel" ].rac_signal
map:^(NSString *name) {
return [self.class fileSystemNameForIdentifier:name];
}];
RACSignal *pathComponents = [directoryURL.pathComponents.rac_signal concat:identifierComponents];
return [NSURL fileURLWithPathComponents:[pathComponents array]];
}]
sqrl_tryCreateDirectory]
setNameWithFormat:@"%@ -applicationSupportURL", self];
}
- (RACSignal *)downloadDirectoryURL {
return [[[[self
applicationSupportURL]
map:^(NSURL *directoryURL) {
return [directoryURL URLByAppendingPathComponent:@"downloads"];
}]
sqrl_tryCreateDirectory]
setNameWithFormat:@"%@ -downloadDirectoryURL", self];
}
- (RACSignal *)createUniqueUpdateDirectoryURL {
return [[[[self
applicationSupportURL]
map:^(NSURL *directoryURL) {
// noindex so that Spotlight doesn't pick up apps pending update and
// add them to the Launch Services database.
return [[directoryURL URLByAppendingPathComponent:NSProcessInfo.processInfo.globallyUniqueString] URLByAppendingPathExtension:@"noindex"];
}]
try:^(NSURL *directoryURL, NSError **errorRef) {
// Explicitly just provide the owner with permission, discarding the
// current umask. This matches the `mkdtemp` behaviour.
NSDictionary *directoryAttributes = @{
NSFilePosixPermissions: @(S_IRWXU),
};
return [NSFileManager.defaultManager createDirectoryAtURL:directoryURL withIntermediateDirectories:YES attributes:directoryAttributes error:errorRef];
}]
setNameWithFormat:@"%@ -createUniqueUpdateDirectoryURL", self];
}
- (RACSignal *)shipItStateURL {
return [[[self
applicationSupportURL]
+21
View File
@@ -0,0 +1,21 @@
//
// SQRLDownload+Private.h
// Squirrel
//
// Created by Keith Duncan on 21/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLDownload.h"
@interface SQRLDownload ()
// Designated initialiser.
//
// request - Must not be nil. The request this download is for.
// fileURL - Must not be nil. The file location to save received data to.
//
// Returns an initialised download suitable for saving response data.
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL;
@end
+31
View File
@@ -0,0 +1,31 @@
//
// SQRLDownload.h
// Squirrel
//
// Created by Keith Duncan on 21/11/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class RACSignal;
// A download is a file which holds the response body for a request and can be
// resumed by a subsequent request if the initial transfer is interrupted.
//
// Don't create a download directly, instead retrieve one from
// `SQRLDownloadManager`.
@interface SQRLDownload : MTLModel
// The `request` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSURLRequest *request;
// The `fileURL` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSURL *fileURL;
// Calculate a request suitable for resuming the download.
//
// Returns a signal which sends an `NSURLRequest` then completes.
@property (readonly, nonatomic) RACSignal *resumableRequest;
@end
+29
View File
@@ -0,0 +1,29 @@
//
// SQRLDownload.m
// Squirrel
//
// Created by Keith Duncan on 21/11/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownload.h"
#import "SQRLDownload+Private.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@implementation SQRLDownload
- (instancetype)initWithRequest:(NSURLRequest *)request fileURL:(NSURL *)fileURL {
return [self initWithDictionary:@{
@keypath(self.request): request,
@keypath(self.fileURL): fileURL,
} error:NULL];
}
- (RACSignal *)resumableRequest {
return [[RACSignal
return:self.request]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+61
View File
@@ -0,0 +1,61 @@
//
// SQRLDownloadController.h
// Squirrel
//
// Created by Keith Duncan on 26/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class SQRLDownload;
@class SQRLResumableDownload;
@class RACSignal;
@class SQRLDirectoryManager;
// Stores metadata for download resumption, and manages the disk locations for
// where they're stored.
@interface SQRLDownloadManager : NSObject
// Default download manager, initializes a download manager with the current
// application directory manager. Stores downloads in
// `directoryManager.downloadDirectoryURL`.
+ (instancetype)defaultDownloadManager;
// Designated initialiser.
//
// directoryManager - Must not be nil.
//
// Returns a download manager which can resume previously started downloads for
// the initialised directory manager's application.
- (instancetype)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
// Clean the resumable download state, removes downloaded data and tracking
// state.
//
// Returns a signal which errors or completes when all file operations have been
// attempted.
- (RACSignal *)removeAllResumableDownloads;
// Retrieve a previously started download, or initialise a new download.
//
// Previously started downloads may have been removed and require
// reinitialising.
//
// request - The request you are performing and require a disk location to save
// the response body to. Must not be nil.
//
// Returns a signal which sends an `SQRLDownload` object then completes, or
// errors.
- (RACSignal *)downloadForRequest:(NSURLRequest *)request;
// Store metadata for a download so that it can be resumed later.
//
// download - The download being stored for future resumption. Must not be nil.
// request - The request for which the download can be resumed. Must not be
// nil.
//
// Returns a signal which completes or errors.
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request;
@end
+239
View File
@@ -0,0 +1,239 @@
//
// SQRLDownloadController.m
// Squirrel
//
// Created by Keith Duncan on 26/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownloadManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SQRLDirectoryManager.h"
#import "SQRLDownload.h"
#import "SQRLDownload+Private.h"
#import "SQRLResumableDownload.h"
#import "NSData+SQRLExtensions.h"
@interface SQRLDownloadManager ()
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
@property (nonatomic, assign, readonly) dispatch_queue_t queue;
// Reads the download index.
//
// Returns a signal which sends the download index at time of read then
// completes, or errors.
- (RACSignal *)readDownloadIndex;
// Write a new download index.
//
// block - Map from the old download index to the new download index. Writing
// is serialised, subsequent writers will receive the output of the
// previous map block. If the output of the map is equal to the input
// no write is attempted and the returned signal completes.
//
// Returns a signal which completes or errors.
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block;
@end
@implementation SQRLDownloadManager
+ (instancetype)defaultDownloadManager {
static SQRLDownloadManager *defaultDownloadManager;
static dispatch_once_t defaultDownloadManagerPredicate;
dispatch_once(&defaultDownloadManagerPredicate, ^{
defaultDownloadManager = [[self alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
});
return defaultDownloadManager;
}
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
NSParameterAssert(directoryManager != nil);
self = [super init];
if (self == nil) return nil;
_directoryManager = directoryManager;
_queue = dispatch_queue_create("com.github.Squirrel.SQRLDownloadManager.queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_set_target_queue(_queue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
return self;
}
- (void)dealloc {
if (_queue != NULL) dispatch_release(_queue);
}
- (RACSignal *)removeAllResumableDownloads {
return [[[RACSignal
merge:@[
[self downloadStoreIndexFileLocation],
[self.directoryManager downloadDirectoryURL]
]]
flattenMap:^ (NSURL *location) {
return [[RACSignal
defer:^{
NSError *error;
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:location error:&error];
return (remove ? [RACSignal empty] : [RACSignal error:error]);
}]
catch:^(NSError *error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
return [RACSignal error:error];
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)downloadStoreIndexFileLocation {
return [[[self.directoryManager
applicationSupportURL]
map:^(NSURL *directory) {
return [directory URLByAppendingPathComponent:@"DownloadIndex.plist"];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)readDownloadIndex {
return [[[self
downloadStoreIndexFileLocation]
flattenMap:^(NSURL *location) {
return [RACSignal
create:^(id<RACSubscriber> subscriber) {
dispatch_async(self.queue, ^{
if (subscriber.disposable.disposed) return;
NSError *error;
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
if (propertyList == nil) {
[subscriber sendError:error];
return;
}
[subscriber sendNext:propertyList];
[subscriber sendCompleted];
});
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)writeDownloadIndexWithBlock:(NSDictionary * (^)(NSMutableDictionary *))block {
NSParameterAssert(block != nil);
return [[[self
downloadStoreIndexFileLocation]
flattenMap:^(NSURL *location) {
return [RACSignal create:^(id<RACSubscriber> subscriber) {
dispatch_barrier_async(self.queue, ^{
if (subscriber.disposable.disposed) return;
NSError *error;
NSDictionary *propertyList = [self readPropertyListWithContentsOfURL:location error:&error];
if (propertyList == nil) {
[subscriber sendError:error];
return;
}
NSDictionary *newPropertyList = block([propertyList mutableCopy]);
if ([newPropertyList isEqual:propertyList]) {
[subscriber sendCompleted];
return;
}
NSData *newData = [NSKeyedArchiver archivedDataWithRootObject:newPropertyList];
if (newData == nil) {
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
[subscriber sendError:[NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListWriteStreamError userInfo:errorInfo]];
return;
}
BOOL write = [newData writeToURL:location options:NSDataWritingAtomic error:&error];
if (!write) {
[subscriber sendError:error];
return;
}
[subscriber sendCompleted];
});
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (NSDictionary *)readPropertyListWithContentsOfURL:(NSURL *)location error:(NSError **)errorRef {
NSData *propertyListData = [NSData dataWithContentsOfURL:location options:NSDataReadingUncached error:NULL];
if (propertyListData == nil) {
return @{};
}
NSDictionary *propertyList = [NSKeyedUnarchiver unarchiveObjectWithData:propertyListData];
if (propertyList == nil) {
if (errorRef != NULL) {
NSDictionary *errorInfo = @{ NSURLErrorKey: location };
*errorRef = [NSError errorWithDomain:NSCocoaErrorDomain code:NSPropertyListReadCorruptError userInfo:errorInfo];
}
return nil;
}
return propertyList;
}
+ (NSString *)keyForURL:(NSURL *)URL {
return URL.absoluteString;
}
+ (NSString *)fileNameForURL:(NSURL *)URL {
NSString *key = [self keyForURL:URL];
return [[[key dataUsingEncoding:NSUTF8StringEncoding] sqrl_SHA1Hash] sqrl_base16String];
}
- (RACSignal *)downloadForRequest:(NSURLRequest *)request {
NSParameterAssert(request != nil);
return [[[[self
readDownloadIndex]
map:^(NSDictionary *index) {
NSString *key = [self.class keyForURL:request.URL];
return index[key];
}]
flattenMap:^(SQRLResumableDownload *download) {
if (download != nil) return [RACSignal return:download];
return [[[self.directoryManager
downloadDirectoryURL]
map:^(NSURL *downloadDirectory) {
return [downloadDirectory URLByAppendingPathComponent:[self.class fileNameForURL:request.URL]];
}]
map:^(NSURL *location) {
return [[SQRLDownload alloc] initWithRequest:request fileURL:location];
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)setDownload:(SQRLResumableDownload *)download forRequest:(NSURLRequest *)request {
NSParameterAssert(download.response != nil);
NSParameterAssert(request != nil);
return [[self
writeDownloadIndexWithBlock:^(NSMutableDictionary *index) {
NSString *key = [self.class keyForURL:request.URL];
if (download != nil) {
index[key] = download;
} else {
[index removeObjectForKey:key];
}
return index;
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+1 -1
View File
@@ -11,7 +11,7 @@
@class SQRLUpdate;
// A SQRLUpdate that has been successfully downloaded to disk.
@interface SQRLDownloadedUpdate : MTLModel
@interface SQRLDownloadedUpdate : MTLModel <MTLJSONSerializing>
// The application bundle representing the downloaded and unarchived update.
@property (nonatomic, strong, readonly) NSBundle *bundle;
+10 -3
View File
@@ -7,6 +7,7 @@
//
#import "SQRLDownloadedUpdate.h"
#import "SQRLUpdate.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLDownloadedUpdate ()
@@ -40,9 +41,15 @@
#pragma mark MTLJSONSerializing
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
}];
return @{};
}
+ (NSValueTransformer *)updateJSONTransformer {
return [MTLValueTransformer mtl_JSONDictionaryTransformerWithModelClass:SQRLUpdate.class];
}
+ (NSValueTransformer *)bundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
@end
-17
View File
@@ -1,17 +0,0 @@
//
// SQRLInstaller+Private.h
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLInstaller.h"
// The defaults key to store a `SQRLInstallerOwnedBundle` so that a moved bundle
// can be restored.
extern NSString * const SQRLInstallerOwnedBundleKey;
// The defaults key to store the number of installation attempts that have been
// made.
extern NSString * const SQRLShipItInstallationAttemptsKey;
+19 -23
View File
@@ -11,7 +11,7 @@
// The domain for errors originating within SQRLInstaller.
extern NSString * const SQRLInstallerErrorDomain;
// There was an error copying the target or update bundle to a backup location.
// There was an error copying the target bundle to the backup location.
extern const NSInteger SQRLInstallerErrorBackupFailed;
// There was an error replacing the target bundle with the update.
@@ -34,39 +34,35 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
// There was an error moving a bundle across volumes.
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
// There was an error changing the file permissions of the update.
extern const NSInteger SQRLInstallerErrorChangingPermissions;
@class RACSignal;
@class RACAction;
@class SQRLShipItState;
@class SQRLDirectoryManager;
@class RACCommand;
// Performs the installation of an update, saving its intermediate state to user
// defaults.
// Performs the installation of an update, using the `SQRLShipItState` on disk,
// as located by `SQRLDirectoryManager`.
//
// This class is meant to be used only after the app that will be updated has
// terminated.
@interface SQRLInstaller : NSObject
// Initializes an installer using the given application identifier, which is
// used to scope resumable state stored to user defaults.
// Attempts to install the update or resume an in-progress installation.
//
// applicationIdentifier - The defaults domain in which to store resumable
// state. Must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)applicationIdentifier;
// When executed with a `SQRLShipItRequest`, attempts to install the update or
// resume an in-progress installation.
//
// Each execution will complete or error on an unspecified scheduler when
// Returns a signal which completes or errors on an unspecified scheduler when
// installation has completed or failed.
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state;
// When executed with a `SQRLShipItRequest`, aborts an installation, and
// attempts to restore the old version of the application if necessary.
// Aborts an installation, and attempts to restore the old version of the
// application if necessary.
//
// This must not be executed while `installUpdateCommand` is executing.
// This must not be executed while `installUpdateWithState:` is executing.
//
// Each execution will complete or error on an unspecified scheduler once
// Returns a signal while completes or errors on an unspecified scheduler once
// aborting/recovery has finished.
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state;
// Initializes an installer using the given directory manager to read and write the
// state of the installation.
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
@end
+349 -362
View File
@@ -7,25 +7,17 @@
//
#import "SQRLInstaller.h"
#import <libkern/OSAtomic.h>
#import "EXTScope.h"
#import <ReactiveCocoa/NSEnumerator+RACSequenceAdditions.h>
#import <ReactiveCocoa/NSObject+RACPropertySubscribing.h>
#import <ReactiveCocoa/RACCommand.h>
#import <ReactiveCocoa/RACSequence.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <sys/xattr.h>
#import "NSBundle+SQRLVersionExtensions.h"
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItRequest.h"
#import "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
#import "SQRLInstallerOwnedBundle.h"
#import <libkern/OSAtomic.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/xattr.h>
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
@@ -36,58 +28,85 @@ const NSInteger SQRLInstallerErrorInvalidBundleVersion = -4;
const NSInteger SQRLInstallerErrorMissingInstallationData = -5;
const NSInteger SQRLInstallerErrorInvalidState = -6;
const NSInteger SQRLInstallerErrorMovingAcrossVolumes = -7;
const NSInteger SQRLInstallerErrorChangingPermissions = -8;
NSString * const SQRLShipItInstallationAttemptsKey = @"SQRLShipItInstallationAttempts";
NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// Maps an installer state to a selector to invoke.
typedef struct {
// The state for which the associated method should be invoked.
SQRLInstallerState installerState;
// A method accepting a `SQRLShipItState` argument and returning a cold
// signal.
//
// If NULL, installation should complete.
SEL selector;
} SQRLInstallerDispatchTableEntry;
@interface SQRLInstaller ()
// The defaults domain to store all resumable state in.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
// Finds the state file to read and write from.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
// The bundle currently owned by this installer.
// Reads the given key from `state`, failing if it's not set.
//
// Stores the bundle moved aside by an install request so that the original
// bundle can be restored to its original location if needed.
@property (atomic, strong) SQRLInstallerOwnedBundle *ownedBundle;
// Reads the given key from `request`, failing if it's not set.
//
// key - The property key to read from `request`. This must not be nil, and
// should refer to a property of object type.
// request - The request object to read. This must not be nil.
// key - The property key to read from `state`. This must not be nil, and
// should refer to a property of object type.
// state - The state object to read. This must not be nil.
//
// Returns a signal which synchronously sends the non-nil read value then
// completes, or errors.
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request;
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state;
// Moves the updateBundleURL to an owned directory to prevent symlink attack,
// takes user:group ownership of the bundle, then verifies that it meets the
// designated requirement of the targetBundleURL.
// Performs the remaining stages of installation, as specified by `state`.
//
// request - The request whose update should be prepared and validated.
// state - The installation state. This must not be nil.
//
// Returns a signal which sends the owned & validated bundle URL then completes,
// or errors.
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request;
// Returns a signal which will complete or error on an unspecified thread.
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state;
// Saves a `SQRLInstallerOwnedBundle` for the targetBundleURL to the
// preferences, then moves the targetBundleURL to an owned directory.
// Moves the specified bundle to a backup location.
//
// request - The request whose target should be removed in preparation of an
// update being installed.
// bundleURL - The URL to the bundle that should be backed up. This must not be
// nil.
//
// Returns a signal which completes, or errors.
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request;
// Returns a signal which will send the `NSURL` to the proposed backup location
// as soon as possible, then complete once the bundle has actually been moved.
- (RACSignal *)backUpBundleAtURL:(NSURL *)bundleURL;
// Deletes a bundle that was moved into place using -moveAndTakeOwnershipOfBundleAtURL:.
// Deletes a bundle that was backed up using -backUpBundleAtURL:.
//
// bundleURL - The URL to the backup bundle, as sent from -moveAndTakeOwnershipOfBundleAtURL:.
// backupURL - The URL to the backup bundle, as sent from -backUpBundleAtURL:.
// This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL;
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL;
// Validates the code signature of a bundle, optionally restoring it upon
// failure.
//
// bundleURL - The URL of the bundle whose code signature should be
// verified. This must not be nil.
// signature - The code signature that the bundle must match. This must
// not be nil.
// backupBundleURL - If not nil, the URL to a bundle that should replace
// `bundleURL` if the code signature does not pass validation.
//
// Returns a signal which will synchronously complete if `bundleURL` passes
// validation, or if the bundle was recovered from `backupBundleURL`. If
// validation or recovery fails, an error will be sent.
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL;
// Attempts to determine whether a bundle has already been moved on disk.
//
// sourceURL - The original URL to the bundle. This must not be nil.
// targetURL - The proposed destination URL for the bundle. This must not be
// nil.
// signature - The code signature that any item must match in order to be
// considered the correct bundle. This must not be nil.
//
// Returns a signal which will synchronously send YES if `targetURL` points to
// a bundle matching the code signature, NO if it doesn't and `sourceURL` still
// exists, or an error otherwise.
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature;
// Moves `sourceURL` to `targetURL`.
//
@@ -99,7 +118,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// sourceURL - The URL to move from. This must not be nil.
//
// Retruns a signal which will synchronously complete or error.
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
// Recursively clears the quarantine extended attribute from the given
// directory.
@@ -113,96 +132,56 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// Returns a signal which will send completed or error on a background thread.
- (RACSignal *)clearQuarantineForDirectory:(NSURL *)directory;
// Recursively changes the owner and group of the given directory tree to that
// of the current process, then disables writing for anyone but the owner.
//
// directoryURL - The URL to the folder to take ownership of. This must not be
// nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL;
@end
@implementation SQRLInstaller
#pragma mark Lifecycle
- (id)initWithApplicationIdentifier:(NSString *)applicationIdentifier {
NSParameterAssert(applicationIdentifier != nil);
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
NSParameterAssert(directoryManager != nil);
self = [super init];
if (self == nil) return nil;
_applicationIdentifier = [applicationIdentifier copy];
@weakify(self);
RACSignal *aborting = [[[[RACObserve(self, abortInstallationCommand)
ignore:nil]
map:^(RACCommand *command) {
return command.executing;
}]
switchToLatest]
setNameWithFormat:@"aborting"];
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItRequest *request) {
@strongify(self);
NSParameterAssert(request != nil);
// Request can be changed between launches, the installer may have
// already have an owned bundle, for a previous targetURL.
//
// If that's the case, we need to abort the previous owned bundle, and
// then handle the new install request.
return [[[[self
abortInstall]
doError:^(NSError *error) {
NSLog(@"Couldn't abort install and restore owned bundle to previous location %@, error %@", self.ownedBundle.originalURL, error.sqrl_verboseDescription);
}]
catchTo:[RACSignal empty]]
then:^{
return [self installRequest:request];
}];
}];
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItRequest *request) {
@strongify(self);
return [self abortInstall];
}];
_directoryManager = directoryManager;
return self;
}
#pragma mark Preferences
#pragma mark Actions
- (SQRLInstallerOwnedBundle *)ownedBundle {
id archiveData = CFBridgingRelease(CFPreferencesCopyValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost));
if (![archiveData isKindOfClass:NSData.class]) return nil;
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
SQRLInstallerOwnedBundle *ownedBundle = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
if (![ownedBundle isKindOfClass:SQRLInstallerOwnedBundle.class]) return nil;
return ownedBundle;
return [[self
resumeInstallationFromState:state]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
- (void)setOwnedBundle:(SQRLInstallerOwnedBundle *)ownedBundle {
NSData *archiveData = (ownedBundle == nil ? nil : [NSKeyedArchiver archivedDataWithRootObject:ownedBundle]);
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFPropertyListRef)archiveData, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
CFPreferencesSynchronize((__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, SQRLCodeSignature *codeSignature) {
return [self verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:state.backupBundleURL];
}]
flatten]
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
#pragma mark Properties
#pragma mark Installer State
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request {
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
NSParameterAssert(key != nil);
NSParameterAssert(request != nil);
NSParameterAssert(state != nil);
return [[RACSignal
defer:^{
id value = [request valueForKey:key];
id value = [state valueForKey:key];
if (value == nil) {
NSString *errorDescription = [NSString stringWithFormat:NSLocalizedString(@"Missing %@", nil), key];
return [RACSignal error:[self missingDataErrorWithDescription:errorDescription]];
@@ -210,210 +189,309 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
return [RACSignal return:value];
}
}]
setNameWithFormat:@"%@ -getRequiredKey: %@ fromRequest: %@", self, key, request];
setNameWithFormat:@"%@ -getRequiredKey: %@ fromState: %@", self, key, state];
}
#pragma mark Installer States
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
const SQRLInstallerDispatchTableEntry dispatchTablePrototype[] = {
{ .installerState = SQRLInstallerStateNothingToDo, .selector = NULL },
{ .installerState = SQRLInstallerStateClearingQuarantine, .selector = @selector(clearQuarantineWithState:) },
{ .installerState = SQRLInstallerStateBackingUp, .selector = @selector(backUpWithState:) },
{ .installerState = SQRLInstallerStateInstalling, .selector = @selector(installWithState:) },
{ .installerState = SQRLInstallerStateVerifyingInPlace, .selector = @selector(verifyInPlaceWithState:) },
};
return [[[[[[[self
ownedTemporaryDirectoryURL]
flattenMap:^(NSURL *directoryURL) {
return [self copyBundleAtURL:request.updateBundleURL toDirectory:directoryURL];
}]
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
NSValue *boxedDispatchTable = [NSValue valueWithBytes:dispatchTablePrototype objCType:@encode(__typeof__(dispatchTablePrototype))];
RACSignal *step = [RACSignal defer:^{
SQRLInstallerDispatchTableEntry dispatchTable[tableCount];
[boxedDispatchTable getValue:&dispatchTable];
SQRLInstallerState installerState = state.installerState;
size_t tableIndex;
for (tableIndex = 0; tableIndex < tableCount; tableIndex++) {
if (dispatchTable[tableIndex].installerState == installerState) {
break;
}
}
if (tableIndex >= tableCount) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"Invalid installer state %i", nil), (int)installerState],
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Try installing the update again.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorInvalidState userInfo:userInfo]];
}
SEL selector = dispatchTable[tableIndex].selector;
if (selector == NULL) {
// Nothing to do.
return [RACSignal empty];
}
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]];
invocation.target = self;
invocation.selector = selector;
SQRLShipItState *stateArg = state;
[invocation setArgument:&stateArg atIndex:2];
[invocation invoke];
__unsafe_unretained RACSignal *step = nil;
[invocation getReturnValue:&step];
SQRLInstallerState nextState;
if (tableIndex + 1 >= tableCount) {
nextState = SQRLInstallerStateNothingToDo;
} else {
nextState = dispatchTable[tableIndex + 1].installerState;
}
return [[[step
doCompleted:^{
NSLog(@"Completed state %i", (int)installerState);
}]
ignoreValues]
concat:[RACSignal return:@(nextState)]];
}];
return [[self
stepRepeatedly:step withState:state]
setNameWithFormat:@"%@ -resumeInstallationFromState: %@", self, state];
}
- (RACSignal *)stepRepeatedly:(RACSignal *)step withState:(SQRLShipItState *)state {
NSParameterAssert(step != nil);
NSParameterAssert(state != nil);
return [step flattenMap:^(NSNumber *nextState) {
state.installerState = nextState.integerValue;
state.installationStateAttempt = 1;
return [[state
writeUsingURL:self.directoryManager.shipItStateURL]
// Automatically begin the next step.
concat:[self stepRepeatedly:step withState:state]];
}];
}
- (RACSignal *)clearQuarantineWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[self
getRequiredKey:@keypath(state.updateBundleURL) fromState:state]
flattenMap:^(NSURL *bundleURL) {
return [[[self
clearQuarantineForDirectory:bundleURL]
ignoreValues]
concat:[RACSignal return:bundleURL]];
return [self clearQuarantineForDirectory:bundleURL];
}]
zipWith:[self codeSignatureForBundleAtURL:request.targetBundleURL]]
reduceEach:^(NSURL *updateBundleURL, SQRLCodeSignature *codeSignature) {
return [[[self
verifyBundleAtURL:updateBundleURL usingSignature:codeSignature]
ignoreValues]
concat:[RACSignal return:updateBundleURL]];
}]
flatten]
setNameWithFormat:@"%@ -prepareAndValidateUpdateBundleURLForRequest: %@", self, request];
setNameWithFormat:@"%@ -clearQuarantineWithState: %@", self, state];
}
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
- (RACSignal *)backUpWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[[RACSignal
zip:@[
[self ownedTemporaryDirectoryURL],
[self codeSignatureForBundleAtURL:request.targetBundleURL],
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
NSURL *targetBundleURL = request.targetBundleURL;
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state],
] reduce:^(NSURL *bundleURL, SQRLCodeSignature *codeSignature) {
RACSignal *skipBackup = [RACSignal return:@NO];
if (state.backupBundleURL != nil) {
skipBackup = [self checkWhetherBundlePreviouslyAtURL:bundleURL wasInstalledAtURL:state.backupBundleURL usingSignature:codeSignature];
}
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
return [skipBackup flattenMap:^(NSNumber *skip) {
if (skip.boolValue) {
return [RACSignal empty];
} else {
return [self backUpBundleAtURL:bundleURL];
}
}];
}]
doNext:^(SQRLInstallerOwnedBundle *ownedBundle) {
self.ownedBundle = ownedBundle;
flatten]
flattenMap:^(NSURL *backupBundleURL) {
// Save the chosen backup URL as soon as we have it, so we
// can resume even if the state change hasn't taken effect.
//
// N.B. It's important that this method remain
// synchronous, so it finishes before returning
// control to -backUpBundleAtURL:. Really, the flow
// here should be refactored so it doesn't matter.
state.backupBundleURL = backupBundleURL;
return [state writeUsingURL:self.directoryManager.shipItStateURL];
}]
flattenMap:^(SQRLInstallerOwnedBundle *ownedBundle) {
return [self installItemToURL:ownedBundle.temporaryURL fromURL:ownedBundle.originalURL];
}]
setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request];
setNameWithFormat:@"%@ -backUpWithState: %@", self, state];
}
- (RACSignal *)installRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
- (RACSignal *)installWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[[self
prepareAndValidateUpdateBundleURLForRequest:request]
flattenMap:^(NSURL *updateBundleURL) {
return [[[[[[[self
acquireTargetBundleURLForRequest:request]
concat:[self installItemToURL:request.targetBundleURL fromURL:updateBundleURL]]
concat:[RACSignal return:request.updateBundleURL]]
concat:[RACSignal return:updateBundleURL]]
concat:[RACSignal defer:^{
return [RACSignal return:self.ownedBundle.temporaryURL];
}]]
flattenMap:^(NSURL *location) {
return [[[self
deleteOwnedBundleAtURL:location]
doError:^(NSError *error) {
NSLog(@"Couldn't remove owned bundle at location %@, error %@", location, error.sqrl_verboseDescription);
}]
catchTo:[RACSignal empty]];
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.updateBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, NSURL *updateBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
return [[[[self
checkWhetherBundlePreviouslyAtURL:updateBundleURL wasInstalledAtURL:targetBundleURL usingSignature:codeSignature]
flattenMap:^(NSNumber *skip) {
if (skip.boolValue) {
return [RACSignal empty];
} else {
return [self installItemAtURL:targetBundleURL fromURL:updateBundleURL];
}
}]
doCompleted:^{
self.ownedBundle = nil;
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to replace bundle %@ with update %@", nil), targetBundleURL, updateBundleURL];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorReplacingTarget toError:error]];
}]
catch:^(NSError *error) {
// Verify that the target bundle didn't get corrupted during
// failure. Try recovering it if it did.
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
// Recovery succeeded, but we still want to pass
// through the original error.
concat:[RACSignal error:error]];
}];
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path]
setNameWithFormat:@"%@ -installRequest: %@", self, request];
flatten]
setNameWithFormat:@"%@ -installWithState: %@", self, state];
}
- (RACSignal *)abortInstall {
// The request may have been tampered with to select a new targetURL to
// which the moved bundles should be restored.
//
// Discard the request parameters and restore the owned bundle to its
// original location.
SQRLInstallerOwnedBundle *ownedBundle = self.ownedBundle;
if (ownedBundle == nil) return [RACSignal empty];
return [[[[self
installItemToURL:ownedBundle.originalURL fromURL:ownedBundle.temporaryURL]
doCompleted:^{
self.ownedBundle = nil;
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), ownedBundle.originalURL.path]
setNameWithFormat:@"%@ -abortInstall", self];
}
#pragma mark Bundle Ownership
- (RACSignal *)ownedTemporaryDirectoryURL {
return [[[RACSignal
defer:^{
NSString *tmpPath = [NSTemporaryDirectory() stringByResolvingSymlinksInPath];
NSString *template = [NSString stringWithFormat:@"%@.XXXXXXXX", self.applicationIdentifier];
char *fullTemplate = strdup([tmpPath stringByAppendingPathComponent:template].UTF8String);
@onExit {
free(fullTemplate);
};
if (mkdtemp(fullTemplate) == NULL) {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
NSURL *URL = [NSURL fileURLWithPath:[NSFileManager.defaultManager stringWithFileSystemRepresentation:fullTemplate length:strlen(fullTemplate)] isDirectory:YES];
return [RACSignal return:URL];
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create temporary folder", nil)];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
setNameWithFormat:@"%@ -ownedDirectoryURL", self];
}
- (RACSignal *)copyBundleAtURL:(NSURL *)bundleURL toDirectory:(NSURL *)directoryURL {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(directoryURL != nil);
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:bundleURL.lastPathComponent];
- (RACSignal *)verifyInPlaceWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
return [[[RACSignal
defer:^{
NSError *error;
BOOL copy = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:newBundleURL error:&error];
if (!copy) return [RACSignal error:error];
return [RACSignal return:newBundleURL];
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.backupBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, NSURL *backupBundleURL, SQRLCodeSignature *codeSignature) {
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
concat:[[self
deleteBackupAtURL:backupBundleURL]
catchTo:[RACSignal empty]]];
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to copy bundle %@ to directory %@", nil), bundleURL, newBundleURL];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
setNameWithFormat:@"%@ -copyBundleAtURL: %@ toDirectory: %@", self, bundleURL, directoryURL];
flatten]
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
}
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
#pragma mark Backing Up
return [[[RACSignal
defer:^{
NSError *error;
if ([NSFileManager.defaultManager removeItemAtURL:bundleURL error:&error]) {
return [RACSignal empty];
} else {
- (RACSignal *)backUpBundleAtURL:(NSURL *)targetBundleURL {
NSParameterAssert(targetBundleURL != nil);
return [[[[[[self.directoryManager
applicationSupportURL]
flattenMap:^(NSURL *applicationSupportURL) {
NSError *error = nil;
NSURL *temporaryDirectoryURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:applicationSupportURL create:YES error:&error];
if (temporaryDirectoryURL == nil) {
return [RACSignal error:error];
}
return [RACSignal return:temporaryDirectoryURL];
}]
then:^{
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create backup folder", nil)];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
map:^(NSURL *temporaryDirectoryURL) {
return [temporaryDirectoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
}]
flattenMap:^(NSURL *backupBundleURL) {
return [[[[self
installItemAtURL:backupBundleURL fromURL:targetBundleURL]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Failed to move bundle %@ to backup location %@", nil), targetBundleURL, backupBundleURL];
return [RACSignal error:[self errorByAddingDescription:description code:SQRLInstallerErrorBackupFailed toError:error]];
}]
ignoreValues]
// Return the backup URL before doing any work, to increase
// fault tolerance.
startWith:backupBundleURL];
}]
setNameWithFormat:@"%@ -backUpBundleAtURL: %@", self, targetBundleURL];
}
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
NSParameterAssert(backupURL != nil);
return [[RACSignal
defer:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
return [RACSignal error:error];
}
// Also remove the temporary directory that the backup lived in.
NSURL *temporaryDirectoryURL = bundleURL.URLByDeletingLastPathComponent;
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
// However, use rmdir() to skip it in case there are other files
// contained within (for whatever reason).
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) == 0) {
return [RACSignal empty];
} else {
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) != 0) {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -deleteOwnedBundleAtURL: %@", self, bundleURL];
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
}
#pragma mark Verification
- (RACSignal *)codeSignatureForBundleAtURL:(NSURL *)URL {
return [[RACSignal
defer:^{
NSError *error;
SQRLCodeSignature *codeSignature = [SQRLCodeSignature signatureWithBundle:URL error:&error];
if (codeSignature == nil) return [RACSignal error:error];
return [RACSignal return:codeSignature];
}]
setNameWithFormat:@"%@ -codeSignatureForBundleAtURL: %@", self, URL];
}
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature {
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(signature != nil);
return [[[self
takeOwnershipOfDirectory:bundleURL]
then:^{
return [signature verifyBundleAtURL:bundleURL];
return [[[signature
verifyBundleAtURL:bundleURL]
catch:^(NSError *error) {
if (backupBundleURL == nil) return [RACSignal error:error];
return [[[RACSignal
defer:^{
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
return [self installItemAtURL:bundleURL fromURL:backupBundleURL];
}]
doCompleted:^{
NSLog(@"Restored backup bundle to %@", bundleURL);
}]
doError:^(NSError *recoveryError) {
NSLog(@"Could not restore backup bundle %@ to %@: %@", backupBundleURL, bundleURL, recoveryError.sqrl_verboseDescription);
}];
}]
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@", self, bundleURL, signature];
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@ recoveringUsingBackupAtURL: %@", self, bundleURL, signature, backupBundleURL];
}
#pragma mark Installation
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
NSParameterAssert(signature != nil);
return [[[[self
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
concat:[RACSignal return:@YES]]
catch:^(NSError *error) {
BOOL directory;
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
// If the source still exists, this isn't an error.
return [RACSignal return:@NO];
} else {
return [RACSignal error:error];
}
}]
setNameWithFormat:@"%@ -checkWhetherBundlePreviouslyAtURL: %@ wasInstalledAtURL: %@", self, sourceURL, targetURL];
}
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
@@ -425,7 +503,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
} else {
int code = errno;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
@@ -466,7 +544,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
return YES;
}];
return enumerator.rac_sequence.signal;
return [enumerator.rac_promise start];
}]
flattenMap:^(NSURL *URL) {
const char *path = URL.path.fileSystemRepresentation;
@@ -477,7 +555,7 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
// file to begin with.
if (code != ENOATTR) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
@@ -490,97 +568,6 @@ NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
setNameWithFormat:@"%@ -clearQuarantineForDirectory: %@", self, directory];
}
#pragma mark File Security
- (RACSignal *)readFileSecurityOfURL:(NSURL *)location {
NSParameterAssert(location != nil);
return [[RACSignal
defer:^{
NSError *error;
NSFileSecurity *fileSecurity;
if (![location getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
return [RACSignal error:error];
}
return [RACSignal return:fileSecurity];
}]
setNameWithFormat:@"%@ -readFileSecurity: %@", self, location];
}
- (RACSignal *)writeFileSecurity:(NSFileSecurity *)fileSecurity toURL:(NSURL *)location {
NSParameterAssert(location != nil);
return [[RACSignal
defer:^{
NSError *error;
if (![location setResourceValue:fileSecurity forKey:NSURLFileSecurityKey error:&error]) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -writeFileSecurity: %@", self, location];
}
- (RACSignal *)takeOwnershipOfDirectory:(NSURL *)directoryURL {
NSParameterAssert(directoryURL != nil);
return [[[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
NSDirectoryEnumerator *enumerator = [NSFileManager.defaultManager enumeratorAtURL:directoryURL includingPropertiesForKeys:@[ NSURLFileSecurityKey ] options:0 errorHandler:^ BOOL (NSURL *url, NSError *error) {
[subscriber sendError:error];
return NO;
}];
return [[enumerator.rac_sequence.signal
startWith:directoryURL]
subscribe:subscriber];
}]
flattenMap:^(NSURL *itemURL) {
return [[[self
readFileSecurityOfURL:itemURL]
flattenMap:^(NSFileSecurity *fileSecurity) {
if (![self takeOwnershipOfFileSecurity:fileSecurity]) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Permissions Error", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Couldn’t update permissions of %@", nil), itemURL.path],
NSURLErrorKey: itemURL
};
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorChangingPermissions userInfo:errorInfo]];
}
return [RACSignal return:fileSecurity];
}]
flattenMap:^(NSFileSecurity *fileSecurity) {
return [self writeFileSecurity:fileSecurity toURL:itemURL];
}];
}]
setNameWithFormat:@"%@ -takeOwnershipOfDirectory: %@", self, directoryURL];
}
- (BOOL)takeOwnershipOfFileSecurity:(NSFileSecurity *)fileSecurity {
CFFileSecurityRef actualFileSecurity = (__bridge CFFileSecurityRef)fileSecurity;
// If ShipIt is running as root, this will change the owner to
// root:wheel.
if (!CFFileSecuritySetOwner(actualFileSecurity, getuid())) return NO;
if (!CFFileSecuritySetGroup(actualFileSecurity, getgid())) return NO;
mode_t fileMode = 0;
if (!CFFileSecurityGetMode(actualFileSecurity, &fileMode)) return NO;
// Remove write permission from group and other, leave executable
// bit as it was for both.
//
// Permissions will be r-(x?)r-(x?) afterwards, with owner
// permissions left as is.
fileMode = (fileMode & ~(S_IWGRP | S_IWOTH));
return CFFileSecuritySetMode(actualFileSecurity, fileMode);
}
#pragma mark Error Handling
- (NSError *)missingDataErrorWithDescription:(NSString *)description {
-37
View File
@@ -1,37 +0,0 @@
//
// SQRLInstallerOwnedBundle.h
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class SQRLCodeSignature;
// Tracks original and temporary locations of a bundle. Should be created and
// serialised before moving a bundle aside.
//
// Can be used to ensure new targetURL requests meet the original bundle at that
// location's code signature, even though it's been moved aside.
@interface SQRLInstallerOwnedBundle : MTLModel
// Designated initialiser.
//
// originalURL - Where the bundle currently resides, and should be restored to
// should an error occur.
// temporaryURL - Where the bundle will be moved to, so that another bundle can
// take its place at originalURL.
// codeSignature - The code signature of the original bundle, so that the
// signature can be used irrespective of where the bundle
// currently resides.
//
// Returns an initialised owned bundle for serializing.
- (instancetype)initWithOriginalURL:(NSURL *)originalURL temporaryURL:(NSURL *)temporaryURL codeSignature:(SQRLCodeSignature *)codeSignature;
@property (readonly, copy, nonatomic) NSURL *originalURL;
@property (readonly, copy, nonatomic) NSURL *temporaryURL;
@property (readonly, copy, nonatomic) SQRLCodeSignature *codeSignature;
@end
-23
View File
@@ -1,23 +0,0 @@
//
// SQRLInstallerOwnedBundle.m
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLInstallerOwnedBundle.h"
#import "EXTKeyPathCoding.h"
@implementation SQRLInstallerOwnedBundle
- (instancetype)initWithOriginalURL:(NSURL *)originalURL temporaryURL:(NSURL *)temporaryURL codeSignature:(SQRLCodeSignature *)codeSignature {
return [self initWithDictionary:@{
@keypath(self.originalURL): originalURL,
@keypath(self.temporaryURL): temporaryURL,
@keypath(self.codeSignature): codeSignature,
} error:NULL];
}
@end
+27
View File
@@ -0,0 +1,27 @@
//
// SQRLResumableDownload.h
// Squirrel
//
// Created by Keith Duncan on 30/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "SQRLDownload.h"
// A download which has already been started and already has partial data.
@interface SQRLResumableDownload : SQRLDownload
// Designated initialiser.
//
// request - See `SQRLDownload`.
// response - HTTP response whose body is being downloaded to `fileURL`.
// Must not be nil.
// fileURL - See `SQRLDownload`.
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL;
// The `response` the receiver was initialised with.
@property (readonly, copy, nonatomic) NSHTTPURLResponse *response;
@end
+66
View File
@@ -0,0 +1,66 @@
//
// SQRLResumableDownload.m
// Squirrel
//
// Created by Keith Duncan on 30/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLResumableDownload.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSHTTPURLResponse+SQRLExtensions.h"
@implementation SQRLResumableDownload
- (instancetype)initWithRequest:(NSURLRequest *)request response:(NSHTTPURLResponse *)response fileURL:(NSURL *)fileURL {
NSParameterAssert(request != nil);
NSParameterAssert(response != nil);
NSParameterAssert(fileURL != nil);
return [self initWithDictionary:@{
@keypath(self.request): request,
@keypath(self.response): response,
@keypath(self.fileURL): fileURL,
} error:NULL];
}
- (BOOL)isEqual:(SQRLResumableDownload *)object {
if (self == object) return YES;
if (![object isKindOfClass:self.class]) return NO;
if (!(self.response == nil && object.response == nil) && ![self responseEqual:object.response]) return NO;
if (![object.fileURL isEqual:self.fileURL]) return NO;
return YES;
}
- (BOOL)responseEqual:(NSHTTPURLResponse *)response {
if (![response.URL isEqual:self.response.URL]) return NO;
if (response.statusCode != self.response.statusCode) return NO;
if (![response.allHeaderFields isEqual:self.response.allHeaderFields]) return NO;
return YES;
}
- (RACSignal *)resumableRequest {
return [[[super
resumableRequest]
map:^ NSURLRequest * (NSURLRequest *request) {
NSHTTPURLResponse *response = self.response;
NSString *ETag = [response sqrl_valueForHTTPHeaderField:@"ETag"];
if (ETag == nil) return request;
NSNumber *alreadyDownloadedSize = nil;
BOOL getAlreadyDownloadedSize = [self.fileURL getResourceValue:&alreadyDownloadedSize forKey:NSURLFileSizeKey error:NULL];
if (!getAlreadyDownloadedSize) return request;
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:ETag forHTTPHeaderField:@"If-Range"];
[newRequest setValue:[NSString stringWithFormat:@"%llu-", alreadyDownloadedSize.unsignedLongLongValue] forHTTPHeaderField:@"Range"];
return newRequest;
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
@end
+14 -11
View File
@@ -13,7 +13,6 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
@@ -22,7 +21,8 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
@implementation SQRLShipItLauncher
+ (NSString *)shipItJobLabel {
return [SQRLDirectoryManager.currentApplicationManager.applicationIdentifier stringByAppendingString:@".ShipIt"];
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
}
+ (RACSignal *)shipItJobDictionary {
@@ -60,6 +60,10 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
#if DEBUG
jobDict[@(LAUNCH_JOBKEY_DEBUG)] = @YES;
#endif
return jobDict;
}]
setNameWithFormat:@"+shipItJobDictionary"];
@@ -67,7 +71,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
create:^(id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
@@ -104,15 +108,16 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return nil;
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}]];
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -122,13 +127,11 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
] reduce:^(NSDictionary *jobDictionary, id authorization) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
@@ -137,7 +140,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
-96
View File
@@ -1,96 +0,0 @@
//
// SQRLShipItRequest.h
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class RACSignal;
// The domain for errors originating within `SQRLShipItRequest`.
extern NSString * const SQRLShipItRequestErrorDomain;
// Errors originating from the `SQRLShipItRequestErrorDomain`.
//
// SQRLShipItRequestErrorMissingRequiredProperty - A required property was `nil`
// upon initialization.
//
// The `userInfo` dictionary for
// this error will contain
// `SQRLShipItStatePropertyErrorKey`.
//
// SQRLShipItRequestErrorUnarchiving - The saved request could not
// be unarchived, possibly
// because it's invalid.
//
// SQRLShipItRequestErrorArchiving - The request object could not
// be archived.
typedef enum : NSInteger {
SQRLShipItRequestErrorMissingRequiredProperty = 1,
SQRLShipItRequestErrorUnarchiving = 2,
SQRLShipItRequestErrorArchiving = 3,
} SQRLShipItRequestError;
// Associated with an `NSString` indicating the required property key that did
// not have a value upon initialization.
extern NSString * const SQRLShipItRequestPropertyErrorKey;
// Constructed and written to disk for `ShipIt` to pick up. This represents a
// single update request from the client's perspective.
@interface SQRLShipItRequest : MTLModel
// Reads a `SQRLShipItState` from disk, at the location specified by the given
// URL signal.
//
// URLSignal - Determines the file location to read from, the signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
// complete, or error.
+ (RACSignal *)readUsingURL:(RACSignal *)URLSignal;
// Writes the receiver to disk, at the location specified by the given URL
// signal.
//
// URLSignal - Determines the file location to write to. The signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeUsingURL:(RACSignal *)URLSignal;
// Designated initialiser.
//
// updateBundleURL - The update bundle which will replace
// targetBundleURL. Must not be nil.
// targetBundleURL - Where the update should be installed, if a bundle
// is already present, the update is checked for
// suitability against this bundle. Must not be nil.
// bundleIdentifier - The bundle identifier that the installer should
// wait for instances of to terminate before
// installing. Can be nil.
// launchAfterInstallation - Whether the updated application should be launched
// after installation.
//
// Returns a request which can be written to disk for ShipIt to read and
// perform.
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation;
// The URL to the downloaded update's app bundle.
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
// The URL to the app bundle that should be replaced with the update.
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
// The bundle identifier of the application being updated.
//
// If not nil, the installer will wait for applications matching this identifier
// (and `targetBundleURL`) to terminate before continuing.
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
// Whether to launch the application after an update is successfully installed.
@property (nonatomic, assign, readonly) BOOL launchAfterInstallation;
@end
-193
View File
@@ -1,193 +0,0 @@
//
// SQRLShipItRequest.m
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLShipItRequest.h"
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
NSString * const SQRLShipItRequestErrorDomain = @"SQRLShipItRequestErrorDomain";
NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestPropertyErrorKey";
@interface SQRLShipItRequest () <MTLJSONSerializing>
@end
@implementation SQRLShipItRequest
#pragma mark Lifecycle
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [super initWithDictionary:dictionary error:error];
if (self == nil) return nil;
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
if ([self valueForKey:key] != nil) return YES;
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
};
*error = [NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorMissingRequiredProperty userInfo:userInfo];
}
return NO;
};
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
return self;
}
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation {
return [self initWithDictionary:@{
@keypath(self.updateBundleURL): updateBundleURL,
@keypath(self.targetBundleURL): targetBundleURL,
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
@keypath(self.launchAfterInstallation): @(launchAfterInstallation),
} error:NULL];
}
#pragma mark Serialization
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{};
}
+ (NSValueTransformer *)updateBundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)targetBundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[[URL
flattenMap:^(NSURL *stateURL) {
NSError *error;
NSData *data = [self readFromURL:stateURL error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
flattenMap:^(NSData *data) {
return [self readFromData:data];
}]
catch:^(NSError *error) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read update request", nil),
};
if (error != nil) {
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
NSUnderlyingErrorKey: error,
}];
}
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorUnarchiving userInfo:userInfo]];
}]
setNameWithFormat:@"+readUsingURL: %@", URL];
}
+ (NSData *)readFromURL:(NSURL *)URL error:(NSError **)errorRef {
__block NSData *data = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateReadingItemAtURL:URL options:NSFileCoordinatorReadingWithoutChanges error:errorRef byAccessor:^(NSURL *newURL) {
data = [NSData dataWithContentsOfURL:newURL options:NSDataReadingUncached error:errorRef];
}];
return data;
}
+ (RACSignal *)readFromData:(NSData *)data {
return [[RACSignal
defer:^{
NSError *error;
NSDictionary *JSONDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (JSONDictionary == nil) {
return [RACSignal error:error];
}
SQRLShipItRequest *request = [MTLJSONAdapter modelOfClass:SQRLShipItRequest.class fromJSONDictionary:JSONDictionary error:&error];
if (request == nil) {
return [RACSignal error:error];
}
return [RACSignal return:request];
}]
setNameWithFormat:@"+readFromData: <NSData %p>", data];
}
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[[RACSignal
zip:@[
URL,
[self serialization]
] reduce:^(NSURL *stateURL, NSData *data) {
NSError *error;
BOOL write = [self writeData:data toURL:stateURL error:&error];
if (!write) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
flatten]
catch:^(NSError *error) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not write update request", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil),
};
if (error != nil) {
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
NSUnderlyingErrorKey: error,
}];
}
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorArchiving userInfo:userInfo]];
}]
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
}
- (BOOL)writeData:(NSData *)data toURL:(NSURL *)URL error:(NSError **)errorRef {
__block BOOL success = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateWritingItemAtURL:URL options:0 error:errorRef byAccessor:^(NSURL *newURL) {
success = [data writeToURL:newURL options:NSDataWritingAtomic error:errorRef];
}];
return success;
}
- (RACSignal *)serialization {
return [[RACSignal
defer:^{
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:self];
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
setNameWithFormat:@"%@ serialization", self];
}
@end
+117
View File
@@ -0,0 +1,117 @@
//
// SQRLShipItState.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-08.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
// The domain for errors originating within `SQRLShipItState`.
extern NSString * const SQRLShipItStateErrorDomain;
// A required property was `nil` upon initialization.
//
// The `userInfo` dictionary for this error will contain
// `SQRLShipItStatePropertyErrorKey`.
extern const NSInteger SQRLShipItStateErrorMissingRequiredProperty;
// The saved state on disk could not be unarchived, possibly because it's
// invalid.
extern const NSInteger SQRLShipItStateErrorUnarchiving;
// The state object could not be archived.
extern const NSInteger SQRLShipItStateErrorArchiving;
// Associated with an NSString indicating the required property key that did not
// have a value upon initialization.
extern NSString * const SQRLShipItStatePropertyErrorKey;
// The current state of the installer, for persistence across relaunches and for
// tolerance of system failures.
//
// SQRLInstallerStateNothingToDo - ShipIt has not started installing yet.
// SQRLInstallerStateClearingQuarantine - Clearing the quarantine flag on the
// update bundle so it can used without
// issue.
// SQRLInstallerStateBackingUp - Backing up the target bundle so it can
// be restored in the event of failure.
// SQRLInstallerStateInstalling - Replacing the target bundle with the
// update bundle.
// SQRLInstallerStateVerifyingInPlace - Verifying that the target bundle is
// still valid after updating.
//
// Note that these values must remain backwards compatible, so ShipIt doesn't
// start up in a weird mode on a newer version.
typedef enum : NSInteger {
SQRLInstallerStateNothingToDo = 0,
SQRLInstallerStateClearingQuarantine,
SQRLInstallerStateBackingUp,
SQRLInstallerStateInstalling,
SQRLInstallerStateVerifyingInPlace,
} SQRLInstallerState;
@class RACSignal;
@class SQRLCodeSignature;
// Encapsulates all the state needed by the ShipIt process.
@interface SQRLShipItState : MTLModel
// Reads a `SQRLShipItState` from disk, at the location specified by the given
// URL signal.
//
// URLSignal - Determines the file location to read from, the signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItState` then
// complete, or error.
+ (RACSignal *)readUsingURL:(RACSignal *)URL;
// Initializes the receiver with the arguments that will not change during
// installation.
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature;
// Writes the receiver to disk, at the location specified by the given URL
// signal.
//
// URL - Determines the file location to write to. The signal should send an
// `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeUsingURL:(RACSignal *)URL;
// The URL to the app bundle that should be replaced with an update.
@property (nonatomic, copy, readonly) NSURL *targetBundleURL;
// The URL to the downloaded update's app bundle.
@property (nonatomic, copy, readonly) NSURL *updateBundleURL;
// A code signature that the update bundle must match in order to be valid.
@property (nonatomic, copy, readonly) SQRLCodeSignature *codeSignature;
// The bundle identifier of the application being updated.
//
// If not nil, the installer will wait for applications matching this identifier
// (and `targetBundleURL`) to terminate before continuing.
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
// The current state of the installer.
@property (atomic, assign) SQRLInstallerState installerState;
// The number of installation attempts that have occurred for the current
// `state`.
@property (atomic, assign) NSUInteger installationStateAttempt;
// Whether to relaunch the application after an update is successfully
// installed.
@property (atomic, assign) BOOL relaunchAfterInstallation;
// The URL where the target bundle has been backed up to before installing the
// update.
//
// This property is set automatically during the course of installation. It
// should not be preset.
@property (atomic, copy) NSURL *backupBundleURL;
@end
+122
View File
@@ -0,0 +1,122 @@
//
// SQRLShipItState.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-08.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLShipItState.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
NSString * const SQRLShipItStateErrorDomain = @"SQRLShipItStateErrorDomain";
NSString * const SQRLShipItStatePropertyErrorKey = @"SQRLShipItStatePropertyErrorKey";
const NSInteger SQRLShipItStateErrorMissingRequiredProperty = 1;
const NSInteger SQRLShipItStateErrorUnarchiving = 2;
const NSInteger SQRLShipItStateErrorArchiving = 3;
@implementation SQRLShipItState
#pragma mark Lifecycle
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [super initWithDictionary:dictionary error:error];
if (self == nil) return nil;
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
if ([self valueForKey:key] != nil) return YES;
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
};
*error = [NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorMissingRequiredProperty userInfo:userInfo];
}
return NO;
};
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
if (!validateKey(@keypath(self.codeSignature))) return nil;
return self;
}
- (id)initWithTargetBundleURL:(NSURL *)targetBundleURL updateBundleURL:(NSURL *)updateBundleURL bundleIdentifier:(NSString *)bundleIdentifier codeSignature:(SQRLCodeSignature *)codeSignature {
return [self initWithDictionary:@{
@keypath(self.targetBundleURL): targetBundleURL,
@keypath(self.updateBundleURL): updateBundleURL,
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
@keypath(self.codeSignature): codeSignature,
} error:NULL];
}
#pragma mark Serialization
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[URL
flattenMap:^(NSURL *stateURL) {
NSError *error = nil;
NSData *data = [NSData dataWithContentsOfURL:stateURL options:NSDataReadingUncached error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
flattenMap:^(NSData *data) {
SQRLShipItState *state = [NSKeyedUnarchiver unarchiveObjectWithData:data];
if (![state isKindOfClass:SQRLShipItState.class]) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read saved state", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while unarchiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorUnarchiving userInfo:userInfo]];
}
return [RACSignal return:state];
}]
setNameWithFormat:@"+readUsingURL: %@", URL];
}
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
RACSignal *serialization = [RACSignal defer:^{
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:self];
if (data == nil) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not save state", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil)
};
return [RACSignal error:[NSError errorWithDomain:SQRLShipItStateErrorDomain code:SQRLShipItStateErrorArchiving userInfo:userInfo]];
}
return [RACSignal return:data];
}];
return [[[RACSignal
zip:@[
URL,
serialization
] reduce:^(NSURL *stateURL, NSData *data) {
NSError *error = nil;
if (![data writeToURL:stateURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
flatten]
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
}
@end
+1 -1
View File
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
@class RACSignal;
+6 -12
View File
@@ -7,14 +7,7 @@
//
#import "SQRLTerminationListener.h"
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/NSArray+RACSequenceAdditions.h>
#import <ReactiveCocoa/RACDisposable.h>
#import <ReactiveCocoa/RACScheduler.h>
#import <ReactiveCocoa/RACSequence.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLTerminationListener ()
@@ -52,7 +45,7 @@
return [[[[RACSignal
defer:^{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
return [apps.rac_sequence signalWithScheduler:RACScheduler.immediateScheduler];
return apps.rac_signal;
}]
filter:^(NSRunningApplication *application) {
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
@@ -67,7 +60,7 @@
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
create:^(id<RACSubscriber> subscriber) {
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, processIdentifier, DISPATCH_PROC_EXIT, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0));
dispatch_source_set_registration_handler(source, ^{
@@ -79,9 +72,10 @@
});
dispatch_resume(source);
return [RACDisposable disposableWithBlock:^{
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
}];
dispatch_release(source);
}]];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
}
+49
View File
@@ -0,0 +1,49 @@
//
// SQRLURLConnection.h
// Squirrel
//
// Created by Keith Duncan on 25/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class SQRLDownloadManager;
@class RACSignal;
// Resumably download a remote resource.
//
// If the resource has previously been downloaded this operation may return the
// URL of an existing file, contingent on the server matching the ETag of the
// original response.
@interface SQRLURLConnection : NSObject
// This is the designated initialiser. The body of the response is streamed to
// disk. If the response includes an ETag the download can be resumed
// automatically by subsequent download operations initialised with a request of
// equal URL.
//
// request - Must not be nil, the request is issued with NSURLConnection.
- (instancetype)initWithRequest:(NSURLRequest *)request __attribute__((nonnull (1)));
// Starts accumulating the resource body in memory, cancels when the
// subscription is disposed.
//
// Returns a signal when sends a tuple of `NSURLResponse` and `NSData`, then
// completes or errors.
- (RACSignal *)retrieve;
// Starts downloading the resource body to disk, resuming from a previous
// download if possible, cancels when the subscription is disposed.
//
// downloadManager - Must be non nil, determines where the downloads will be
// stored and resumed from. For a previously started download
// to be resumed, an equivalent download manager should be
// provided.
//
// Returns a signal which sends a tuple of `NSURLResponse` and a file
// scheme `NSURL` where the resource has been stored on disk, then completes, or
// errors.
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager __attribute__((nonnull (1)));
@end
+257
View File
@@ -0,0 +1,257 @@
//
// SQRLURLConnection.m
// Squirrel
//
// Created by Keith Duncan on 25/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLURLConnection.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
@interface SQRLURLConnection ()
// The request the downloader was initialised with.
@property (nonatomic, copy, readonly) NSURLRequest *request;
// Listens for invocations of `selector` on the receiver.
//
// The first argument of `selector` must be an `NSURLConnection` object.
//
// Returns a signal which sends the _second_ argument of `selector`, or
// RACUnit if there is only one argument, then completes when the receiver
// deallocates.
- (RACSignal *)signalForDelegateSelector:(SEL)selector;
// Subscribes to the connection delegate selectors and starts a connection for
// `request`.
//
// request - The request to perform, must not be nil.
// mapData - For each response, map the data received for that response to a
// result.
//
// Returns a signal which sends a tuple of the last response, and the first
// value from the last `mapData`, then completes, or errors.
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData;
@end
@implementation SQRLURLConnection
- (instancetype)initWithRequest:(NSURLRequest *)request {
NSParameterAssert(request != nil);
self = [self init];
if (self == nil) return nil;
_request = [request copy];
return self;
}
- (RACSignal *)signalForDelegateSelector:(SEL)selector {
NSParameterAssert(selector != NULL);
return [[self
rac_signalForSelector:selector]
map:^(RACTuple *args) {
return args[1] ?: RACUnit.defaultUnit;
}];
}
- (RACSignal *)connectionSignalWithRequest:(NSURLRequest *)request mapData:(RACSignal * (^)(NSURLResponse *, RACSignal *))mapData {
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
// A signal that will error if the connection fails for any reason.
RACSignal *errors = [[self
signalForDelegateSelector:@selector(connection:didFailWithError:)]
flattenMap:^(NSError *error) {
return [RACSignal error:error];
}];
// Sends (or replays) RACUnit when the connection has finished
// loading successfully.
RACSignal *finished = [[[[self
signalForDelegateSelector:@selector(connectionDidFinishLoading:)]
take:1]
promiseOnScheduler:RACScheduler.immediateScheduler]
start];
// A signal of all `NSURLResponse`s received on the connection.
RACSignal *responses = [[self
signalForDelegateSelector:@selector(connection:didReceiveResponse:)]
takeUntil:finished];
// A signal of all `NSData` received on the connection.
RACSignal *data = [[self
signalForDelegateSelector:@selector(connection:didReceiveData:)]
takeUntil:finished];
[[[[[RACSignal
merge:@[ responses, errors ]]
takeUntil:finished]
map:^(NSURLResponse *response) {
return [RACSignal
zip:@[
[RACSignal return:response],
mapData(response, data),
]];
}]
switchToLatest]
subscribe:subscriber];
NSOperationQueue *delegateQueue = [[NSOperationQueue alloc] init];
delegateQueue.maxConcurrentOperationCount = 1;
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
connection.delegateQueue = delegateQueue;
[connection start];
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
[connection cancel];
}]];
}]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), request];
}
#pragma mark Download
- (RACSignal *)truncateDownload:(SQRLDownload *)download {
NSParameterAssert(download != nil);
return [[[[RACSignal
defer:^{
NSError *error = nil;
BOOL remove = [NSFileManager.defaultManager removeItemAtURL:download.fileURL error:&error];
return (remove ? [RACSignal empty] : [RACSignal error:error]);
}]
catch:^(NSError *error) {
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileNoSuchFileError) return [RACSignal empty];
return [RACSignal error:error];
}]
concat:[RACSignal return:download]]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
}
- (RACSignal *)recordDownload:(SQRLResumableDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager {
NSParameterAssert(download != nil);
NSParameterAssert(downloadManager != nil);
return [[[downloadManager
setDownload:download forRequest:self.request]
concat:[RACSignal return:download]]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), download];
}
- (RACSignal *)prepareDownload:(SQRLDownload *)download downloadManager:(SQRLDownloadManager *)downloadManager forResponse:(NSURLResponse *)response {
return [[RACSignal
defer:^{
if (![response isKindOfClass:NSHTTPURLResponse.class]) {
return [self truncateDownload:download];
}
NSHTTPURLResponse *httpResponse = (id)response;
RACSignal *downloadSignal;
if (httpResponse.statusCode != 206 /* Partial Data */) {
downloadSignal = [self truncateDownload:download];
} else {
downloadSignal = [RACSignal empty];
}
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:self.request response:httpResponse fileURL:download.fileURL];
return [[downloadSignal
ignoreValues]
concat:[self recordDownload:newDownload downloadManager:downloadManager]];
}]
setNameWithFormat:@"%@ %s %@", self, sel_getName(_cmd), response];
}
- (BOOL)appendData:(NSData *)data toURL:(NSURL *)fileURL error:(NSError **)errorRef {
NSOutputStream *outputStream = [NSOutputStream outputStreamWithURL:fileURL append:YES];
[outputStream open];
@onExit {
[outputStream close];
};
uint8_t const *bytes = data.bytes;
size_t length = data.length;
while (length > 0) {
NSInteger written = [outputStream write:bytes maxLength:length];
if (written == -1) {
NSError *streamError = outputStream.streamError;
if ([streamError.domain isEqualToString:NSPOSIXErrorDomain] && streamError.code == EINTR) continue;
if (errorRef != NULL) *errorRef = streamError;
return NO;
}
bytes += written;
length -= written;
}
return YES;
}
#pragma mark Start
- (RACSignal *)retrieve {
return [[self
connectionSignalWithRequest:self.request mapData:^(id _, RACSignal *data) {
return [data
aggregateWithStart:[NSMutableData data] reduce:^(NSMutableData *running, NSData *data) {
[running appendData:data];
return running;
}];
}]
setNameWithFormat:@"%@ %s", self, sel_getName(_cmd)];
}
- (RACSignal *)download:(SQRLDownloadManager *)downloadManager {
NSParameterAssert(downloadManager != nil);
RACSignal *download = [[[downloadManager
downloadForRequest:self.request]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred];
return [[[download
flattenMap:^(SQRLDownload *download) {
return [download resumableRequest];
}]
flattenMap:^(NSURLRequest *request) {
return [self connectionSignalWithRequest:request mapData:^(NSURLResponse *response, RACSignal *data) {
RACSignal *downloadURL = [[[[download
flattenMap:^(SQRLDownload *download) {
return [self prepareDownload:download downloadManager:downloadManager forResponse:response];
}]
map:^(SQRLDownload *download) {
return download.fileURL;
}]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred];
return [[[[data
map:^(NSData *bodyData) {
return [[downloadURL
try:^(NSURL *fileURL, NSError **errorRef) {
return [self appendData:bodyData toURL:fileURL error:errorRef];
}]
ignoreValues];
}]
concat]
ignoreValues]
concat:downloadURL];
}];
}]
setNameWithFormat:@"%@ download: %@", self, downloadManager];
}
@end
+10 -8
View File
@@ -14,6 +14,8 @@ NSString * const SQRLUpdateJSONReleaseNotesKey = @"notes";
NSString * const SQRLUpdateJSONNameKey = @"name";
NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
NSString * const SQRLUpdateErrorDomain = @"SQRLUpdateErrorDomain";
@implementation SQRLUpdate
#pragma mark Lifecycle
@@ -50,7 +52,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
@@ -61,10 +63,10 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{
@keypath(SQRLUpdate.new, releaseNotes): @"notes",
@keypath(SQRLUpdate.new, releaseName): @"name",
@keypath(SQRLUpdate.new, releaseDate): @"pub_date",
@keypath(SQRLUpdate.new, updateURL): @"url",
@keypath(SQRLUpdate.new, releaseNotes): SQRLUpdateJSONReleaseNotesKey,
@keypath(SQRLUpdate.new, releaseName): SQRLUpdateJSONNameKey,
@keypath(SQRLUpdate.new, releaseDate): SQRLUpdateJSONPublicationDateKey,
@keypath(SQRLUpdate.new, updateURL): SQRLUpdateJSONURLKey,
};
}
@@ -94,8 +96,8 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
}
// If neither match, try removing the ':' in the time zone
static NSRegularExpression *timeZoneSuffix = nil;
static dispatch_once_t timeZoneSuffixPredicate = 0;
static NSRegularExpression *timeZoneSuffix;
static dispatch_once_t timeZoneSuffixPredicate;
dispatch_once(&timeZoneSuffixPredicate, ^ {
timeZoneSuffix = [NSRegularExpression regularExpressionWithPattern:@"([-+])([0-9]{2}):([0-9]{2})$" options:0 error:NULL];
});
@@ -142,7 +144,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
+5 -29
View File
@@ -6,23 +6,6 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
// Represents the current state of the updater.
//
// SQRLUpdaterStateIdle - Doing absolutely diddly squat.
// SQRLUpdaterStateCheckingForUpdate - Checking for any updates from the server.
// SQRLUpdaterStateDownloadingUpdate - Update found, downloading the archive.
// SQRLUpdaterStateAwaitingRelaunch - Awaiting a relaunch to install
// the update.
typedef enum : NSUInteger {
SQRLUpdaterStateIdle,
SQRLUpdaterStateCheckingForUpdate,
SQRLUpdaterStateDownloadingUpdate,
SQRLUpdaterStateAwaitingRelaunch,
} SQRLUpdaterState;
// The domain for errors originating within SQRLUpdater.
extern NSString * const SQRLUpdaterErrorDomain;
@@ -60,7 +43,7 @@ extern NSString * const SQRLUpdaterServerDataErrorKey;
// error with code `SQRLUpdaterErrorInvalidJSON` is generated.
extern NSString * const SQRLUpdaterJSONObjectErrorKey;
@class RACCommand;
@class RACAction;
@class RACDisposable;
@class RACSignal;
@@ -69,19 +52,12 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Kicks off a check for updates.
//
// If an update is available, it will be sent on `updates` once downloaded.
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
// The current state of the manager.
//
// This property is KVO-compliant.
@property (atomic, readonly) SQRLUpdaterState state;
// If an update is available, it will be sent on
// `[checkForUpdatesAction deferred]` and `updates` once downloaded.
@property (nonatomic, strong, readonly) RACAction *checkForUpdatesAction;
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
// update is available.
//
// This signal is actually just `checkForUpdatesCommand.executionSignals`,
// flattened for convenience.
// update is available. Completes when the receiver deallocates.
@property (nonatomic, strong, readonly) RACSignal *updates;
// The request that will be sent to check for updates.
+205 -231
View File
@@ -15,11 +15,15 @@
#import "SQRLDirectoryManager.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLUpdate.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLZipArchiver.h"
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "SQRLURLConnection.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
@@ -32,13 +36,10 @@ const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@interface SQRLUpdater ()
@property (atomic, readwrite) SQRLUpdaterState state;
// The directory manager used for downloads and unpacks.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@@ -49,16 +50,6 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// 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.
@@ -78,22 +69,25 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// errors, on a background thread.
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update;
// Downloads the archived bundle associated with the given update.
// Downloads the update archive for the given update.
//
// update - Describes the update to install. This must not be nil.
// downloadDirectory - A directory in which to create a temporary directory for this
// download. This must not be nil.
// downloadDirectory - A directory in which to create a temporary directory for
// this download. This must not be nil.
//
// Returns a signal which sends an unarchived `NSBundle` then completes, or
// errors, on a background thread.
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
// Returns a signal which sends an NSURL of the downloaded update archive then
// completes, or errors, on a background thread.
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory;
// Creates a unique directory in which to save the update bundle, for later use
// by ShipIt.
// Unpacks an update archive.
//
// Returns a signal which sends an `NSURL` then completes, or errors, on an
// unspecified thread.
- (RACSignal *)uniqueTemporaryDirectoryForUpdate;
// archiveURL - The location of the update archive, currently only ZIP
// archives are supported. Must not be nil.
// directoryURL - A directory in which to place the unpacked contents of the
// archive. Must not be nil.
//
// Returns a signal which completes or errors.
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL;
// Recursively searches the given directory for an application bundle that has
// the same identifier as the running application.
@@ -126,16 +120,27 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// Returns a signal which completes or errors on a background thread.
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update;
// Verifies that an existing state is innocuous, and therefore safe to
// overwrite.
//
// This won't be the case if, for example, an update is currently being
// installed.
//
// Returns a signal which sends `existingState` then completes upon successful
// validation, or errors otherwise.
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState;
@end
@implementation SQRLUpdater
@implementation SQRLUpdater {
RACSubject *_updates;
}
#pragma mark Properties
- (RACSignal *)updates {
return [[self.checkForUpdatesCommand.executionSignals
concat]
setNameWithFormat:@"%@ -updates", self];
return [_updates
deliverOn:RACScheduler.mainThreadScheduler];
}
#pragma mark Lifecycle
@@ -154,110 +159,29 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
_updateRequest = [updateRequest copy];
_updateClass = SQRLUpdate.class;
_directoryManager = SQRLDirectoryManager.currentApplicationManager;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
if (_signature == nil) {
#if DEBUG
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
return nil;
#else
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
#endif
}
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_prunedUpdateDirectories = [[[[RACSignal
_checkForUpdatesAction = [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
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.rac_sequence.signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
}]
doNext:^(NSURL *directoryURL) {
NSError *error = nil;
if (![manager removeItemAtURL:directoryURL error:&error]) {
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
}
}];
flattenMap:^(NSURLRequest *request) {
@strongify(self);
return [self checkForUpdates:request];
}]
replayLazily]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
action];
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
_updates = [[RACSubject
subject]
setNameWithFormat:@"%@ updates", self];
// TODO: Maybe allow this to be an argument to the command?
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
// Prune old updates before the first update check.
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
then:^{
self.state = SQRLUpdaterStateCheckingForUpdate;
return [NSURLConnection rac_sendAsynchronousRequest:request];
}]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [[RACSignal
defer:^{
self.state = SQRLUpdaterStateDownloadingUpdate;
return [self downloadAndPrepareUpdate:update];
}]
doCompleted:^{
self.state = SQRLUpdaterStateAwaitingRelaunch;
}];
}]
finally:^{
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return;
self.state = SQRLUpdaterStateIdle;
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
_shipItLauncher = [[[RACSignal
_shipItLauncher = [[[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
@@ -269,31 +193,80 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
replayLazily]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
setNameWithFormat:@"shipItLauncher"];
return self;
}
- (void)dealloc {
[_updates sendCompleted];
}
#pragma mark Checking for Updates
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval {
@weakify(self);
return [[[[[RACSignal
return [[[[RACSignal
interval:interval onScheduler:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]]
flattenMap:^(id _) {
@strongify(self);
return [[self.checkForUpdatesCommand
execute:RACUnit.defaultUnit]
return [[[self.checkForUpdatesAction
deferred]
ignoreValues]
catch:^(NSError *error) {
NSLog(@"Error checking for updates: %@", error);
return [RACSignal empty];
}];
}]
takeUntil:self.rac_willDeallocSignal]
publish]
connect];
subscribeCompleted:^{}];
}
- (RACSignal *)checkForUpdates:(NSURLRequest *)request {
NSParameterAssert(request != nil);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
if (updatesDisabled) return [RACSignal empty];
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:newRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
}
- (RACSignal *)updateFromJSONData:(NSData *)data {
@@ -335,14 +308,21 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
setNameWithFormat:@"%@ -updateFromJSONData:", self];
}
#pragma Download
- (RACSignal *)downloadAndPrepareUpdate:(SQRLUpdate *)update {
NSParameterAssert(update != nil);
return [[[self
uniqueTemporaryDirectoryForUpdate]
return [[[self.directoryManager
createUniqueUpdateDirectoryURL]
flattenMap:^(NSURL *downloadDirectory) {
return [[[self
downloadBundleForUpdate:update intoDirectory:downloadDirectory]
return [[[[[[self
downloadArchiveForUpdate:update intoDirectory:downloadDirectory]
flattenMap:^(NSURL *updateArchiveURL) {
return [self unpackArchiveAtURL:updateArchiveURL intoDirectory:downloadDirectory];
}]
ignoreValues]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
flattenMap:^(NSBundle *updateBundle) {
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
}]
@@ -356,95 +336,65 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
}
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
- (RACSignal *)downloadArchiveForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
NSParameterAssert(update != nil);
NSParameterAssert(downloadDirectory != nil);
return [[[[[RACSignal
return [[[RACSignal
defer:^{
NSURL *zipDownloadURL = update.updateURL;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
return [[[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:zipDownloadRequest];
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:self.directoryManager];
return [[[[connection
download:downloadManager]
reduceEach:^(NSURLResponse *response, NSURL *bodyLocation) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [RACSignal return:bodyData];
return [[RACSignal
return:bodyLocation]
tryMap:^ NSURL * (NSURL *bodyLocation, NSError **errorRef) {
NSURL *updateLocation = [downloadDirectory URLByAppendingPathComponent:response.suggestedFilename];
if (![NSFileManager.defaultManager moveItemAtURL:bodyLocation toURL:updateLocation error:errorRef]) return nil;
return updateLocation;
}];
}]
flatten]
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];
}
}];
concat:[[downloadManager
removeAllResumableDownloads]
catchTo:RACSignal.empty]];
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
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);
}
}];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
}]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
setNameWithFormat:@"%@ -downloadArchiveForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
#pragma mark File Management
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
return [[[RACSignal
- (RACSignal *)unpackArchiveAtURL:(NSURL *)archiveURL intoDirectory:(NSURL *)directoryURL {
NSParameterAssert(archiveURL != nil);
NSParameterAssert(directoryURL != nil);
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
return [SQRLZipArchiver unzipArchiveAtURL:archiveURL intoDirectoryAtURL:directoryURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
};
if (mkdtemp(updateDirectoryCString) == NULL) {
int code = errno;
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not create temporary directory", nil),
NSURLErrorKey: updateDirectoryTemplate
};
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
}
NSString *updateDirectoryPath = [NSFileManager.defaultManager stringWithFileSystemRepresentation:updateDirectoryCString length:strlen(updateDirectoryCString)];
return [RACSignal return:[NSURL fileURLWithPath:updateDirectoryPath isDirectory:YES]];
}]
setNameWithFormat:@"%@ -uniqueTemporaryDirectoryForUpdate", self];
setNameWithFormat:@"%@ unpackArchiveAtURL: %@ intoDirectory: %@", self, archiveURL, directoryURL];
}
- (RACSignal *)updateBundleMatchingCurrentApplicationInDirectory:(NSURL *)directory {
@@ -457,25 +407,28 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, directory, error);
return YES;
}];
NSURL *updateBundleURL = [enumerator.rac_sequence objectPassingTest:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
return NO;
}
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
NSURL *updateBundleURL = [[[enumerator.rac_promise
start]
filter:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
return NO;
}
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}];
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}]
first];
if (updateBundleURL != nil) {
return [RACSignal return:updateBundleURL];
@@ -510,42 +463,62 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [[[[self.signature
verifyBundleAtURL:updateBundle.bundleURL]
then:^{
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle];
return [RACSignal return:downloadedUpdate];
}]
concat:[RACSignal return:[[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle]]]
flattenMap:^(SQRLDownloadedUpdate *downloadedUpdate) {
return [[self prepareUpdateForInstallation:downloadedUpdate] then:^{
return [RACSignal return:downloadedUpdate];
}];
return [[self
prepareUpdateForInstallation:downloadedUpdate]
concat:[RACSignal return:downloadedUpdate]];
}]
setNameWithFormat:@"%@ -verifyAndPrepareUpdate: %@ fromBundle: %@", self, update, updateBundle];
}
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState {
return [[RACSignal
defer:^{
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
// If this happens, shit is crazy, because it implies that an
// update is being installed over us right now.
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal return:existingState];
}]
setNameWithFormat:@"%@ -validateExistingState: %@", self, existingState];
}
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update {
NSParameterAssert(update != nil);
return [[[[RACSignal
defer:^{
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO];
return [request writeUsingURL:self.shipItStateURL];
}]
then:^{
return self.shipItLauncher;
return [[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
ignoreValues]
concat:[RACSignal defer:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
return [state writeUsingURL:self.shipItStateURL];
}]]
concat:self.shipItLauncher]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing update", nil) description:NSLocalizedString(@"An update for %@ is being prepared. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier]
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[[SQRLShipItRequest
return [[[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
map:^(SQRLShipItRequest *request) {
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES];
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [[request
flattenMap:^(SQRLShipItState *state) {
state.relaunchAfterInstallation = YES;
return [[state
writeUsingURL:self.shipItStateURL]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
}]
@@ -556,7 +529,8 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
// Never allow `completed` to escape this signal chain (in case
// -terminate: is asynchronous or something crazy).
concat:[RACSignal never]]
replay]
promiseOnScheduler:RACScheduler.immediateScheduler]
deferred]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
+12 -14
View File
@@ -130,25 +130,23 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
// This is important because the signals on `self` complete upon
// dealloc.
return:self]
then:^{
return [RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
ignoreValues]
concat:[RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}];
}]
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}]]
take:1]
flatten]
replay]
setNameWithFormat:@"-launchWithArguments: %@", arguments];
self.dittoTask.arguments = arguments;
+97 -111
View File
@@ -7,19 +7,15 @@
//
#import <Foundation/Foundation.h>
#import "EXTScope.h"
#import <ReactiveCocoa/RACCommand.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACScheduler.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLInstaller+Private.h"
#import "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
#import "SQRLShipItRequest.h"
// The maximum number of times ShipIt should run the same installation state, in
// an attempt to update.
@@ -31,116 +27,19 @@ static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
static NSUInteger installationAttempts(NSString *applicationIdentifier) {
return CFPreferencesGetAppIntegerValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFStringRef)applicationIdentifier, NULL);
}
static BOOL setInstallationAttempts(NSString *applicationIdentifier, NSUInteger attempts) {
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(attempts), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
}
static BOOL clearInstallationAttempts(NSString *applicationIdentifier) {
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, NULL, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
}
// Waits for all instances of the target application (as described in the
// `request`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItRequest *request) {
// `state`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
return [[RACSignal
defer:^{
if (request.bundleIdentifier == nil) return [RACSignal empty];
if (state.bundleIdentifier == nil) return [RACSignal empty];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
return [listener waitForTermination];
}]
setNameWithFormat:@"waitForTerminationIfNecessary"];
}
static void installRequest(RACSignal *readRequestSignal, SQRLDirectoryManager *directoryManager) {
NSString *applicationIdentifier = directoryManager.applicationIdentifier;
[[[[[readRequestSignal
flattenMap:^(SQRLShipItRequest *request) {
return waitForTerminationIfNecessary(request);
}]
ignoreValues]
concat:readRequestSignal]
flattenMap:^(SQRLShipItRequest *request) {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
NSUInteger attempt = installationAttempts(applicationIdentifier) + 1;
setInstallationAttempts(applicationIdentifier, attempt);
RACSignal *action;
if (attempt > SQRLShipItMaximumInstallationAttempts) {
action = [[[installer.abortInstallationCommand
execute:request]
initially:^{
NSLog(@"Too many attempts to install, aborting update");
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
// Exit successfully so launchd doesn't restart us
// again.
return [RACSignal empty];
}];
} else {
action = [[[[installer.installUpdateCommand
execute:request]
initially:^{
BOOL freshInstall = (attempt == 1);
if (freshInstall) {
NSLog(@"Beginning installation");
} else {
NSLog(@"Resuming installation attempt %i", (int)attempt);
}
}]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path];
}
// Clear the installation attempts for a successful abort or
// install.
action = [action doCompleted:^{
clearInstallationAttempts(applicationIdentifier);
}];
if (request.launchAfterInstallation) {
// Launch regardless of whether installation succeeds or fails.
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
finally:^{
NSURL *bundleURL = request.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot launch application");
return;
}
NSError *error;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application launched at %@", bundleURL);
}];
}
return action;
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
@@ -152,13 +51,100 @@ int main(int argc, const char * argv[]) {
return EXIT_FAILURE;
}
char const *jobLabel = argv[1];
const char *jobLabel = argv[1];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
RACSignal *stateLocation = directoryManager.shipItStateURL;
installRequest([SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL], directoryManager);
[[[[[[[SQRLShipItState
readUsingURL:stateLocation]
flattenMap:^(SQRLShipItState *state) {
return waitForTerminationIfNecessary(state);
}]
ignoreValues]
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
concat:[SQRLShipItState readUsingURL:stateLocation]]
catch:^(NSError *error) {
NSLog(@"Error reading saved installer state: %@", error);
// Exit successfully so launchd doesn't restart us again.
return [RACSignal empty];
}]
flattenMap:^(SQRLShipItState *state) {
BOOL freshInstall = (state.installerState == SQRLInstallerStateNothingToDo);
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:directoryManager];
NSUInteger attempt = (freshInstall ? 1 : state.installationStateAttempt + 1);
RACSignal *action;
if (attempt > SQRLShipItMaximumInstallationAttempts) {
action = [[RACSignal
defer:^{
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
return [installer abortInstallationWithState:state];
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
// Exit successfully so launchd doesn't restart us again.
return [RACSignal empty];
}];
} else {
action = [[[[RACSignal
defer:^{
if (freshInstall) {
NSLog(@"Beginning installation");
state.installerState = SQRLInstallerStateClearingQuarantine;
} else {
NSLog(@"Resuming installation from state %i", (int)state.installerState);
}
state.installationStateAttempt = attempt;
return [state writeUsingURL:stateLocation];
}]
concat:[installer installUpdateWithState:state]]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
if (state.relaunchAfterInstallation) {
// Relaunch regardless of whether installation succeeds or
// fails.
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
doFinished:^{
NSURL *bundleURL = state.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot relaunch application");
return;
}
NSError *error = nil;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
NSLog(@"Could not relaunch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application relaunched at %@", bundleURL);
}];
}
return action;
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
dispatch_main();
}
return EXIT_SUCCESS;
}
+8
View File
@@ -0,0 +1,8 @@
//
// Prefix header for all source files of the 'Squirrel' target in the 'Squirrel' project
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SystemConfiguration/SystemConfiguration.h>
#endif
-8
View File
@@ -6,14 +6,6 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Squirrel.
FOUNDATION_EXPORT double SquirrelVersionNumber;
//! Project version string for Squirrel.
FOUNDATION_EXPORT const unsigned char SquirrelVersionString[];
#import <Squirrel/NSBundle+SQRLVersionExtensions.h>
#import <Squirrel/NSProcessInfo+SQRLVersionExtensions.h>
#import <Squirrel/SQRLDownloadedUpdate.h>
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
@@ -0,0 +1,23 @@
//
// NSHTTPURLResponseExtensionsSpec.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "NSHTTPURLResponse+SQRLExtensions.h"
SpecBegin(NSHTTPURLResponseExtensions)
it(@"should perform case insensitive key look up", ^{
NSDictionary *headers = @{
@"ETag": @"foo",
};
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://localhost"] statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:headers];
expect([response sqrl_valueForHTTPHeaderField:@"etag"]).to.equal(@"foo");
expect([response sqrl_valueForHTTPHeaderField:@"ETAG"]).to.equal(@"foo");
});
SpecEnd
+7 -12
View File
@@ -6,35 +6,30 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(NSProcessInfoExtensions)
SpecBegin(NSProcessInfoExtensions)
describe(@"-sqrl_operatingSystemShortVersionString", ^{
__block NSString *versionString;
beforeEach(^{
versionString = NSProcessInfo.processInfo.sqrl_operatingSystemShortVersionString;
expect(versionString).notTo(beNil());
expect(versionString).notTo.beNil();
});
it(@"should follow major.minor.patch format", ^{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[0-9]+(\\.[0-9]+)*$" options:0 error:&error];
expect(regex).notTo(beNil());
expect(error).to(beNil());
expect(regex).notTo.beNil();
expect(error).to.beNil();
NSRange fullRange = NSMakeRange(0, versionString.length);
NSRange matchRange = [regex rangeOfFirstMatchInString:versionString options:0 range:fullRange];
expect(@(NSEqualRanges(matchRange, fullRange))).to(beTruthy());
expect(NSEqualRanges(matchRange, fullRange)).to.beTruthy();
});
it(@"should be numerically comparable", ^{
expect(@([@"10.6.9" compare:versionString options:NSNumericSearch])).to(equal(@(NSOrderedAscending)));
expect([@"10.6.9" compare:versionString options:NSNumericSearch]).to.equal(NSOrderedAscending);
});
});
QuickSpecEnd
SpecEnd
@@ -6,16 +6,9 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLCodeSignature.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLCodeSignatureSpec)
SpecBegin(SQRLCodeSignature)
__block NSBundle *bundle;
__block void (^corruptURL)(NSURL *URL);
@@ -24,26 +17,26 @@ beforeEach(^{
bundle = self.testApplicationBundle;
corruptURL = ^(NSURL *URL) {
expect(@([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL])).to(beTruthy());
expect([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL]).to.beTruthy();
};
});
it(@"should verify a valid bundle", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should fail to verify with different code signing requirements", ^{
NSError *error = nil;
SQRLCodeSignature *signature = [SQRLCodeSignature currentApplicationSignature:&error];
expect(signature).notTo(beNil());
expect(error).to(beNil());
SQRLCodeSignature *verifier = [SQRLCodeSignature currentApplicationSignature:&error];
expect(verifier).notTo.beNil();
expect(error).to.beNil();
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(error).notTo(beNil());
BOOL success = [[verifier verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
});
describe(@"code signature changes", ^{
@@ -54,15 +47,15 @@ describe(@"code signature changes", ^{
});
it(@"should fail to verify a bundle with a missing code signature", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt code signature", ^{
@@ -70,25 +63,25 @@ describe(@"code signature changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
describe(@"main executable changes", ^{
it(@"should fail to verify a bundle with a missing executable", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorCouldNotCreateStaticCode)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
});
it(@"should fail to verify a bundle with a corrupt executable", ^{
@@ -96,11 +89,11 @@ describe(@"main executable changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -109,19 +102,19 @@ describe(@"helper executable changes", ^{
beforeEach(^{
helperURL = [bundle URLForAuxiliaryExecutable:@"unused-helper"];
expect(helperURL).notTo(beNil());
expect(helperURL).notTo.beNil();
});
it(@"should fail to verify a bundle with a missing helper", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:helperURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:helperURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt helper", ^{
@@ -129,11 +122,11 @@ describe(@"helper executable changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -142,19 +135,19 @@ describe(@"resource changes", ^{
beforeEach(^{
resourceURL = [bundle URLForResource:@"MainMenu" withExtension:@"nib"];
expect(resourceURL).notTo(beNil());
expect(resourceURL).notTo.beNil();
});
it(@"should fail to verify a bundle with a missing resource", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:resourceURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:resourceURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt resource", ^{
@@ -162,11 +155,11 @@ describe(@"resource changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
@@ -178,15 +171,15 @@ describe(@"framework changes", ^{
});
it(@"should fail to verify a bundle with a missing framework", ^{
expect(@([NSFileManager.defaultManager removeItemAtURL:frameworkURL error:NULL])).to(beTruthy());
expect([NSFileManager.defaultManager removeItemAtURL:frameworkURL error:NULL]).to.beTruthy();
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
it(@"should fail to verify a bundle with a corrupt framework", ^{
@@ -194,12 +187,12 @@ describe(@"framework changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
});
});
QuickSpecEnd
SpecEnd
+9 -16
View File
@@ -6,14 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLDeepCodesign)
SpecBegin(SQRLDeepCodesign)
NSMutableDictionary * (^environmentSuitableForChildProcess)(void) = ^ {
// Remove environment variables that configure the Obj-C runtime
@@ -49,7 +42,7 @@ void (^resignTestApplicationPreserveEverythingButTheRequirements)(void) = ^{
[resignCodesignTask launch];
[resignCodesignTask waitUntilExit];
expect(@(resignCodesignTask.terminationStatus)).to(equal(@0));
expect(resignCodesignTask.terminationStatus).to.equal(0);
};
void (^deepCodesignTestApplication)(void) = ^{
@@ -61,9 +54,9 @@ void (^deepCodesignTestApplication)(void) = ^{
NSNumber *executable = nil;
NSError *executableError = nil;
BOOL getExecutable = [deepCodesignLocation getResourceValue:&executable forKey:NSURLIsExecutableKey error:&executableError];
expect(@(getExecutable)).to(beTruthy());
expect(@(executable.boolValue)).to(beTruthy());
expect(executableError).to(beNil());
expect(getExecutable).to.beTruthy();
expect(executable.boolValue).to.beTruthy();
expect(executableError).to.beNil();
NSTask *deepCodesignTask = [[NSTask alloc] init];
deepCodesignTask.launchPath = deepCodesignLocation.path;
@@ -82,7 +75,7 @@ void (^deepCodesignTestApplication)(void) = ^{
[deepCodesignTask launch];
[deepCodesignTask waitUntilExit];
expect(@(deepCodesignTask.terminationStatus)).to(equal(@0));
expect(deepCodesignTask.terminationStatus).to.equal(0);
/*
By signing test application's contents, which are covered by test
@@ -118,9 +111,9 @@ it(@"should deep sign the test application", ^{
});
xit(@"should deep verify after signing", ^{
expect(@(deepVerify())).to(beFalsy());
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(@(deepVerify())).to(beTruthy());
expect(deepVerify()).to.beTruthy();
});
QuickSpecEnd
SpecEnd
+13 -18
View File
@@ -6,31 +6,26 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
QuickSpecBegin(SQRLDirectoryManagerSpec)
SpecBegin(SQRLDirectoryManager)
__block NSString *otestIdentifier;
beforeEach(^{
otestIdentifier = NSProcessInfo.processInfo.environment[@"FORCE_APP_IDENTIFIER"];
expect(otestIdentifier).notTo(beNil());
otestIdentifier = NSRunningApplication.currentApplication.localizedName;
expect(otestIdentifier).notTo.beNil();
});
it(@"should initialize", ^{
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier];
expect(manager).notTo(beNil());
expect(manager).notTo.beNil();
});
it(@"should create a manager for the current app", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
expect(manager).notTo(beNil());
expect(manager).to(equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]));
expect(manager).notTo.beNil();
expect(manager).to.equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]);
});
it(@"should send an Application Support URL", ^{
@@ -38,12 +33,12 @@ it(@"should send an Application Support URL", ^{
NSError *error = nil;
NSURL *appSupportURL = [[manager applicationSupportURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo(beNil());
expect(error).to(beNil());
expect(appSupportURL).notTo.beNil();
expect(error).to.beNil();
__block BOOL directory = NO;
expect(@([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory])).to(beTruthy());
expect(@(directory)).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory]).to.beTruthy();
expect(directory).to.beTruthy();
});
it(@"should send a ShipIt state URL", ^{
@@ -51,8 +46,8 @@ it(@"should send a ShipIt state URL", ^{
NSError *error = nil;
NSURL *stateURL = [[manager shipItStateURL] firstOrDefault:nil success:NULL error:&error];
expect(stateURL).notTo(beNil());
expect(error).to(beNil());
expect(stateURL).notTo.beNil();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+87
View File
@@ -0,0 +1,87 @@
//
// SQRLDownloadControllerSpec.m
// Squirrel
//
// Created by Keith Duncan on 27/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
#import "NSError+SQRLVerbosityExtensions.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
SpecBegin(SQRLDownloadManager)
__block SQRLDownloadManager *downloadManager = nil;
beforeAll(^{
downloadManager = SQRLDownloadManager.defaultDownloadManager;
NSError *removeError = nil;
BOOL remove = [[downloadManager removeAllResumableDownloads] waitUntilCompleted:&removeError];
if (!remove) {
if ([removeError.domain isEqualToString:NSCocoaErrorDomain] && removeError.code == NSFileNoSuchFileError) return;
NSLog(@"Couldn’t remove resumable downloads %@", removeError.sqrl_verboseDescription);
}
});
NSURL * (^newTestURL)() = ^ () {
return [[NSURL alloc] initWithScheme:@"http" host:@"localhost" path:[@"/" stringByAppendingString:NSProcessInfo.processInfo.globallyUniqueString]];
};
NSURL * (^newDownloadURL)() = ^ () {
NSError *error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:newTestURL()]] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSURL *downloadURL = download.fileURL;
expect(downloadURL).notTo.beNil();
return downloadURL;
};
it(@"should return a file that doesn't exist yet for new URLs", ^{
NSURL *downloadURL = newDownloadURL();
BOOL exists = [NSFileManager.defaultManager fileExistsAtPath:downloadURL.path];
expect(exists).to.beFalsy();
});
it(@"should return a path in a writable directory for new URLs", ^{
NSURL *downloadURL = newDownloadURL();
BOOL writable = [NSFileManager.defaultManager isWritableFileAtPath:downloadURL.URLByDeletingLastPathComponent.path];
expect(writable).to.beTruthy();
});
it(@"should return the same path for the same URL", ^{
NSURL *testURL = newTestURL();
SQRLDownload *download1 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
SQRLDownload *download2 = [[downloadManager downloadForRequest:[NSURLRequest requestWithURL:testURL]] first];
expect(download1).notTo.beNil();
expect(download1).to.equal(download2);
});
it(@"should remember a response", ^{
NSURLRequest *request = [NSURLRequest requestWithURL:newTestURL()];
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:@{ @"ETag": NSProcessInfo.processInfo.globallyUniqueString }];
SQRLDownload *initialDownload = [[downloadManager downloadForRequest:request] first];
SQRLResumableDownload *newDownload = [[SQRLResumableDownload alloc] initWithRequest:request response:response fileURL:initialDownload.fileURL];
expect(newDownload).notTo.equal(initialDownload);
NSError *error = nil;
BOOL setNewDownload = [[downloadManager setDownload:newDownload forRequest:request] waitUntilCompleted:&error];
expect(setNewDownload).to.beTruthy();
expect(error).to.beNil();
SQRLResumableDownload *resumedDownload = [[downloadManager downloadForRequest:request] first];
expect(newDownload).to.equal(resumedDownload);
});
SpecEnd
+40
View File
@@ -0,0 +1,40 @@
//
// SQRLDownloadedUpdateSpec.m
// Squirrel
//
// Created by Keith Duncan on 04/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLDownloadedUpdate)
it(@"should serialise to JSON", ^{
NSError *error = nil;
SQRLUpdate *update = [SQRLUpdate modelWithDictionary:@{
@keypath(SQRLUpdate.new, updateURL) : [NSURL URLWithString:@"http://fake.domain/path"],
} error:&error];
expect(update).notTo.beNil();
NSURL *testApplicationURL = [self createTestApplicationUpdate];
NSBundle *testApplicationBundle = [NSBundle bundleWithURL:testApplicationURL];
expect(testApplicationBundle).notTo.beNil();
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:testApplicationBundle];
expect(downloadedUpdate).notTo.beNil();
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:downloadedUpdate];
expect(JSONDictionary).notTo.beNil();
NSData *JSONData = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
expect(JSONData).notTo.beNil();
expect(error).to.beNil();
JSONDictionary = [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:&error];
expect(JSONDictionary).notTo.beNil();
SQRLDownloadedUpdate *downloadedUpdate2 = [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:&error];
expect(downloadedUpdate2).to.equal(downloadedUpdate);
expect(error).to.beNil();
});
SpecEnd
+18
View File
@@ -0,0 +1,18 @@
//
// SQRLHTTPServer.h
// Squirrel
//
// Created by Keith Duncan on 03/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface SQRLHTTPServer : NSObject
- (NSURL *)start:(NSError **)errorRef;
- (void)invalidate;
@property (copy, nonatomic) CFHTTPMessageRef (^responseBlock)(CFHTTPMessageRef request);
@end
+165
View File
@@ -0,0 +1,165 @@
//
// SQRLHTTPServer.m
// Squirrel
//
// Created by Keith Duncan on 03/12/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLHTTPServer.h"
#import <sys/socket.h>
#import <netinet/in.h>
@interface SQRLHTTPServer ()
@property (assign, nonatomic) dispatch_source_t listenerSource;
@property (strong, nonatomic) NSMutableArray *cleanupBlocks;
@end
@implementation SQRLHTTPServer
- (id)init {
self = [super init];
if (self == nil) return nil;
_cleanupBlocks = [[NSMutableArray alloc] init];
return self;
}
- (void)dealloc {
[self invalidate];
}
- (NSURL *)start:(NSError **)errorRef {
NSParameterAssert(self.listenerSource == NULL);
int listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
struct sockaddr_in listenAddress = {
.sin_len = sizeof(listenAddress),
.sin_family = AF_INET,
.sin_port = 0,
.sin_addr = {
.s_addr = htonl(INADDR_LOOPBACK),
},
};
int bindError = bind(listenSocket, (struct sockaddr const *)&listenAddress, listenAddress.sin_len);
if (bindError != 0) {
close(listenSocket);
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:bindError userInfo:nil];
return nil;
}
listen(listenSocket, 128);
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, listenSocket, 0, NULL);
dispatch_source_set_event_handler(source, ^{
int connectionSocket = accept(listenSocket, NULL, NULL);
// Wait to close the connection until after the test case is complete.
// We need NSURLConnection to timeout, closing the connection causes it
// to error immediately.
[self addCleanupBlock:^{
close(connectionSocket);
}];
CFHTTPMessageRef request = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true));
while (1) {
uint8_t buffer;
ssize_t readLength = read(connectionSocket, &buffer, 1);
if (readLength < 0) {
return;
}
CFHTTPMessageAppendBytes(request, (UInt8 const *)&buffer, readLength);
if (!CFHTTPMessageIsHeaderComplete(request)) {
continue;
}
// Doesn't support reading requests with a body for simplicity
NSString *contentLength = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Content-Length")));
if (contentLength != nil) {
return;
}
break;
}
NSLog(@"Received Request:");
NSLog(@"%@", [[NSString alloc] initWithData:CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)) encoding:NSASCIIStringEncoding]);
CFHTTPMessageRef response = self.responseBlock(request);
NSData *responseData = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(response));
NSLog(@"Sending Response:");
NSLog(@"%@", [[NSString alloc] initWithData:responseData encoding:NSASCIIStringEncoding]);
size_t bufferLength = responseData.length;
uint8_t const *buffer = responseData.bytes;
while (1) {
ssize_t writeLength = write(connectionSocket, buffer, bufferLength);
if (writeLength < 0) {
return;
}
buffer += writeLength;
bufferLength -= writeLength;
if (bufferLength == 0) {
break;
}
}
});
dispatch_source_set_cancel_handler(source, ^{
close(listenSocket);
});
dispatch_resume(source);
struct sockaddr_storage localAddress = {};
socklen_t localAddressLength = sizeof(localAddress);
int localAddressError = getsockname(listenSocket, (struct sockaddr *)&localAddress, &localAddressLength);
if (localAddressError != 0) {
dispatch_release(source);
if (errorRef != NULL) *errorRef = [NSError errorWithDomain:NSPOSIXErrorDomain code:localAddressError userInfo:nil];
return nil;
}
self.listenerSource = source;
// IPv4 and IPv6 address transport layer port fields are at the same offset
// and are the same size
in_port_t port = ntohs(((struct sockaddr_in *)&localAddress)->sin_port);
return [NSURL URLWithString:[NSString stringWithFormat:@"http://localhost:%ld/", (unsigned long)port]];
}
- (void)invalidate {
dispatch_source_t listenerSource = self.listenerSource;
if (listenerSource == NULL) return;
dispatch_source_cancel(listenerSource);
dispatch_release(listenerSource);
self.listenerSource = NULL;
[self performCleanup];
}
- (void)addCleanupBlock:(void (^)(void))block {
[self.cleanupBlocks addObject:[block copy]];
}
- (void)performCleanup {
NSArray *blocks = [self.cleanupBlocks copy];
[self.cleanupBlocks removeAllObjects];
for (void (^currentBlock)(void) in blocks) {
currentBlock();
}
}
@end
+91 -128
View File
@@ -6,32 +6,13 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller+Private.h"
#import "SQRLInstallerOwnedBundle.h"
#import "SQRLShipItRequest.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLInstallerSpec)
mode_t (^modeOfURL)(NSURL *) = ^ mode_t (NSURL *fileURL) {
NSFileSecurity *fileSecurity = nil;
BOOL success = [fileURL getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:NULL];
expect(@(success)).to(beTruthy());
expect(fileSecurity).notTo(beNil());
__block mode_t mode;
expect(@(CFFileSecurityGetMode((__bridge CFFileSecurityRef)fileSecurity, &mode))).to(beTruthy());
return mode & (S_IRWXU | S_IRWXG | S_IRWXO);
};
SpecBegin(SQRLInstaller)
__block NSURL *updateURL;
@@ -39,148 +20,127 @@ beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
it(@"should install an update using ShipIt", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
it(@"should install an update", ^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self installWithRequest:request remote:YES];
[self launchShipIt];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should install an update in process", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
[self installWithRequest:request remote:NO];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should install an update and relaunch", ^{
NSString *bundleIdentifier = @"com.github.Squirrel.TestApplication";
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier];
expect(@(apps.count)).to(equal(@0));
expect(apps.count).to.equal(0);
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.relaunchAfterInstallation = YES;
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self installWithRequest:request remote:YES];
[self launchShipIt];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count)).toEventually(equal(@1));
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count).will.equal(1);
});
it(@"should install an update from another volume", ^{
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication 2.1" fromDirectory:updateURL.URLByDeletingLastPathComponent];
updateURL = [diskImageURL URLByAppendingPathComponent:updateURL.lastPathComponent];
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self installWithRequest:request remote:YES];
[self launchShipIt];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should install an update to another volume", ^{
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication" fromDirectory:self.testApplicationURL.URLByDeletingLastPathComponent];
NSURL *targetURL = [diskImageURL URLByAppendingPathComponent:self.testApplicationURL.lastPathComponent];
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self installWithRequest:request remote:YES];
[self launchShipIt];
NSURL *plistURL = [targetURL URLByAppendingPathComponent:@"Contents/Info.plist"];
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"with backup restoration", ^{
__block NSURL *targetURL;
it(@"should install an update in process", ^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.installerState = SQRLInstallerStateClearingQuarantine;
beforeEach(^{
targetURL = self.testApplicationURL;
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
expect(installer).notTo.beNil();
NSURL *copiedTargetURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication Target.app"];
expect(@([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:copiedTargetURL error:NULL])).to(beTruthy());
SQRLCodeSignature *codeSignature = self.testApplicationSignature;
SQRLInstallerOwnedBundle *ownedBundle = [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:targetURL temporaryURL:copiedTargetURL codeSignature:codeSignature];
NSData *ownedBundleArchive = [NSKeyedArchiver archivedDataWithRootObject:ownedBundle];
expect(ownedBundleArchive).notTo(beNil());
// Set up ShipIt's preferences like it paused in the middle of an
// installation.
NSString *applicationIdentifier = self.shipItDirectoryManager.applicationIdentifier;
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(4), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFDataRef)ownedBundleArchive, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
BOOL synchronized = CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
expect(@(synchronized)).to(beTruthy());
});
it(@"should not install an update after too many attempts", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
[self installWithRequest:request remote:YES];
__block NSError *error;
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).toEventually(beTruthy());
expect(error).to(beNil());
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
it(@"should relaunch even after failing to install an update", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES];
[self installWithRequest:request remote:YES];
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count)).toEventually(equal(@1));
__block NSError *error;
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).to(beTruthy());
expect(error).to(beNil());
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
NSError *installError = nil;
BOOL install = [[installer installUpdateWithState:state] asynchronouslyWaitUntilCompleted:&installError];
expect(install).to.beTruthy();
expect(installError).to.beNil();
});
it(@"should disallow writing the updated application except by the owner", ^{
NSString *command = [NSString stringWithFormat:@"chmod -R 0777 '%@'", updateURL.path];
expect(@(system(command.UTF8String))).to(equal(@0));
it(@"should not install an update after too many attempts", ^{
NSURL *targetURL = self.testApplicationURL;
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
expect(@(modeOfURL(updateURL))).to(equal(@0777));
expect(@(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0777));
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.backupBundleURL = backupURL;
state.installerState = SQRLInstallerStateInstalling;
state.installationStateAttempt = 4;
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
// No update should've been installed, and the application should be
// restored from the backup.
__block NSError *error = nil;
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
expect(error).to.beNil();
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
expect(@(modeOfURL(self.testApplicationURL))).to(equal(@0755));
expect(@(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0755));
it(@"should relaunch even after failing to install an update", ^{
NSURL *targetURL = self.testApplicationURL;
NSURL *backupURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app.bak"];
expect([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:backupURL error:NULL]).to.beTruthy();
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.backupBundleURL = backupURL;
state.installerState = SQRLInstallerStateInstalling;
state.installationStateAttempt = 4;
state.relaunchAfterInstallation = YES;
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self launchShipIt];
__block NSError *error = nil;
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
expect(error).to.beNil();
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
describe(@"signal handling", ^{
__block NSURL *targetURL;
void (^verifyUpdate)(void) = ^{
// Wait up to the launchd throttle interval, then verify that ShipIt
// relaunched and finished installing the update.
expect(self.testApplicationBundleVersion).withTimeout(5).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
NSError *error;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
};
beforeEach(^{
// Copied so that we don't recreate the TestApplication bundle by
// accessing the property.
targetURL = self.testApplicationURL;
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
[self installWithRequest:request remote:YES];
[self launchShipIt];
// Wait until ShipIt has transitioned by at least one state.
expect([[[SQRLShipItState readUsingURL:self.shipItDirectoryManager.shipItStateURL] asynchronousFirstOrDefault:nil success:NULL error:NULL] installerState]).willNot.equal(SQRLInstallerStateNothingToDo);
// Apply a random delay before sending the termination signal, to
// fuzz out race conditions.
@@ -188,42 +148,45 @@ describe(@"signal handling", ^{
[NSThread sleepForTimeInterval:delay];
});
afterEach(^{
// Wait up to the launchd throttle interval, then verify that ShipIt
// relaunched and finished installing the update.
Expecta.asynchronousTestTimeout = 5;
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should handle SIGHUP", ^{
system("killall -HUP ShipIt");
verifyUpdate();
});
it(@"should handle SIGTERM", ^{
system("killall -TERM ShipIt");
verifyUpdate();
});
it(@"should handle SIGINT", ^{
system("killall -INT ShipIt");
verifyUpdate();
});
it(@"should handle SIGQUIT", ^{
system("killall -QUIT ShipIt");
verifyUpdate();
});
it(@"should handle SIGKILL", ^{
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
// a couple times to really test resumption.
//
// Two SIGKILL signals means that it'll be launched three times (which
// matches the maximum number of attempts per state).
for (int i = 0; i < 2; i++) {
// a few times to really test resumption.
for (int i = 0; i < 3; i++) {
system("killall -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
[NSThread sleepForTimeInterval:delay];
}
verifyUpdate();
});
});
QuickSpecEnd
SpecEnd
-86
View File
@@ -1,86 +0,0 @@
//
// SQRLShipItStateSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-09.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLShipItRequest.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLShipItRequestSpec)
__block SQRLDirectoryManager *directoryManager;
__block SQRLShipItRequest *request;
beforeEach(^{
directoryManager = SQRLDirectoryManager.currentApplicationManager;
NSURL *updateURL = [self createTestApplicationUpdate];
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
expect(request).notTo(beNil());
expect(request.targetBundleURL).to(equal(self.testApplicationURL));
expect(request.updateBundleURL).to(equal(updateURL));
expect(request.bundleIdentifier).to(beNil());
expect(@(request.launchAfterInstallation)).to(beFalsy());
});
afterEach(^{
NSURL *stateURL = [[directoryManager shipItStateURL] first];
expect(stateURL).notTo(beNil());
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
});
it(@"should copy", ^{
SQRLShipItRequest *requestCopy = [request copy];
expect(requestCopy).to(equal(request));
expect(requestCopy).notTo(beIdenticalTo(request));
});
it(@"should fail to read state when no file exists yet", ^{
NSError *error;
BOOL success = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(error).notTo(beNil());
});
it(@"should write and read state to disk", ^{
NSError *error;
BOOL success = [[request writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(readRequest).to(equal(request));
});
it(@"should fail gracefully with archives encoding a different class", ^{
NSURL *archiveLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"archive"];
NSError *error;
BOOL write = [[NSKeyedArchiver archivedDataWithRootObject:@"rogue object"] writeToURL:archiveLocation atomically:YES];
expect(@(write)).to(beTruthy());
expect(error).to(beNil());
BOOL success = NO;
SQRLShipItRequest *request = [[SQRLShipItRequest readUsingURL:[RACSignal return:archiveLocation]] firstOrDefault:nil success:&success error:&error];
expect(request).to(beNil());
expect(@(success)).to(beFalsy());
expect(error.domain).to(equal(SQRLShipItRequestErrorDomain));
expect(@(error.code)).to(equal(@(SQRLShipItRequestErrorUnarchiving)));
});
QuickSpecEnd
+63
View File
@@ -0,0 +1,63 @@
//
// SQRLShipItStateSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-09.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDirectoryManager.h"
#import "SQRLShipItState.h"
SpecBegin(SQRLShipItState)
__block SQRLDirectoryManager *directoryManager;
__block SQRLShipItState *state;
beforeEach(^{
directoryManager = SQRLDirectoryManager.currentApplicationManager;
NSURL *updateURL = [self createTestApplicationUpdate];
state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect(state).notTo.beNil();
expect(state.targetBundleURL).to.equal(self.testApplicationURL);
expect(state.updateBundleURL).to.equal(updateURL);
expect(state.bundleIdentifier).to.beNil();
expect(state.codeSignature).to.equal(self.testApplicationSignature);
});
afterEach(^{
NSURL *stateURL = [[directoryManager shipItStateURL] firstOrDefault:nil success:NULL error:NULL];
expect(stateURL).notTo.beNil();
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
});
it(@"should copy", ^{
SQRLShipItState *stateCopy = [state copy];
expect(stateCopy).to.equal(state);
expect(stateCopy).notTo.beIdenticalTo(state);
});
it(@"should fail to read state when no file exists yet", ^{
NSError *error = nil;
BOOL success = [[SQRLShipItState readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
});
it(@"should write and read state", ^{
NSError *error = nil;
BOOL success = [[state writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
SQRLShipItState *readState = [[SQRLShipItState readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(readState).to.equal(state);
});
SpecEnd
+13 -20
View File
@@ -6,22 +6,15 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLTerminationListener.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLTerminationListenerSpec)
SpecBegin(SQRLTerminationListener)
__block SQRLTerminationListener *listener;
beforeEach(^{
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
expect(listener).notTo(beNil());
expect(listener).notTo.beNil();
});
it(@"should complete immediately when the app is not running", ^{
@@ -30,7 +23,7 @@ it(@"should complete immediately when the app is not running", ^{
completed = YES;
}];
expect(@(completed)).to(beTruthy());
expect(completed).to.beTruthy();
});
it(@"should wait until one instance terminates", ^{
@@ -44,12 +37,12 @@ it(@"should wait until one instance terminates", ^{
completed = YES;
}];
expect(observedApp).toEventually(equal(app));
expect(@(completed)).to(beFalsy());
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
[app forceTerminate];
expect(@(app.terminated)).toEventually(beTruthy());
expect(@(completed)).toEventually(beTruthy());
expect(app.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
it(@"should wait until multiple instances terminate", ^{
@@ -61,15 +54,15 @@ it(@"should wait until multiple instances terminate", ^{
completed = YES;
}];
expect(@(completed)).to(beFalsy());
expect(completed).to.beFalsy();
[app1 forceTerminate];
expect(@(app1.terminated)).toEventually(beTruthy());
expect(@(completed)).to(beFalsy());
expect(app1.terminated).will.beTruthy();
expect(completed).to.beFalsy();
[app2 forceTerminate];
expect(@(app2.terminated)).toEventually(beTruthy());
expect(@(completed)).toEventually(beTruthy());
expect(app2.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
QuickSpecEnd
SpecEnd
@@ -1,16 +1,11 @@
//
// QuickSpec+SQRLFixtures.h
// SQRLTestCase.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
// The short version string for the `testApplicationBundle`.
extern NSString * const SQRLTestApplicationOriginalShortVersionString;
@@ -23,9 +18,8 @@ extern NSString * const SQRLBundleShortVersionStringKey;
@class SQRLCodeSignature;
@class SQRLDirectoryManager;
@class SQRLShipItRequest;
@interface QuickSpec (SQRLFixtures)
@interface SQRLTestCase : SPTSenTestCase
// A URL to a temporary directory tests can use.
//
@@ -51,6 +45,10 @@ extern NSString * const SQRLBundleShortVersionStringKey;
// A code signature with requirements from TestApplication.app.
@property (nonatomic, strong, readonly) SQRLCodeSignature *testApplicationSignature;
// A serialized `SecRequirementRef` representing the requirements from
// TestApplication.app.
@property (nonatomic, copy, readonly) NSData *testApplicationCodeSigningRequirementData;
// A directory manager for finding URLs that apply to ShipIt.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *shipItDirectoryManager;
@@ -76,8 +74,8 @@ extern NSString * const SQRLBundleShortVersionStringKey;
// deleted at the end of the example.
- (NSURL *)createTestApplicationUpdate;
// Runs the installer in process or submits ShipIt's launchd job to start it up.
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
// Submits ShipIt's launchd job to start it up.
- (void)launchShipIt;
// Creates a disk image, then mounts it.
//
@@ -1,20 +1,16 @@
//
// QuickSpec+SQRLFixtures.m
// SQRLTestCase.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLTestCase.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItRequest.h"
#import <ServiceManagement/ServiceManagement.h>
#import <objc/runtime.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
@@ -43,9 +39,105 @@ static void SQRLSignalHandler(int sig) {
abort();
}
@interface SQRLTestCase ()
// An array of dispatch_block_t values, for performing cleanup after the current
// example finishes.
//
// This array will be emptied between each example.
@property (nonatomic, strong, readonly) NSMutableArray *exampleCleanupBlocks;
// The URL to the temporary directory which contains `temporaryDirectoryURL` and
// all copied test data.
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
// Returns an _unlaunched_ task that will follow log files at the given paths,
// then pipe that output through this process.
static NSTask *SQRLTailTaskWithPaths(RACSequence *paths) {
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths;
@end
@implementation SQRLTestCase
#pragma mark Properties
@synthesize baseTemporaryDirectoryURL = _baseTemporaryDirectoryURL;
#pragma mark Lifecycle
+ (void)load {
NSURL *appSupportURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
NSAssert(appSupportURL != nil, @"Could not find Application Support folder");
NSArray *folders = @[
@"com.github.Squirrel.TestApplication.ShipIt",
@"otest.ShipIt",
@"otest-x86_64.ShipIt",
];
RACSignal *logLocations = [[[[folders.rac_signal
flattenMap:^(NSString *folder) {
return [folder stringsByAppendingPaths:@[
@"ShipIt_stdout.log",
@"ShipIt_stderr.log",
]].rac_signal;
}]
map:^(NSString *path) {
return [appSupportURL URLByAppendingPathComponent:path];
}]
doNext:^(NSURL *location) {
[[NSData data] writeToURL:location atomically:YES];
}]
map:^(NSURL *location) {
return location.path;
}];
NSTask *readShipIt = [self tailTaskWithPaths:logLocations];
[readShipIt launch];
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
atexit_b(^{
[readShipIt terminate];
});
}
- (void)setUp {
[super setUp];
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
Expecta.asynchronousTestTimeout = 3;
}
- (void)SPT_setUp {
_exampleCleanupBlocks = [[NSMutableArray alloc] init];
}
- (void)SPT_tearDown {
NSArray *cleanupBlocks = [self.exampleCleanupBlocks copy];
_exampleCleanupBlocks = nil;
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
block();
}
}
- (void)tearDown {
[super tearDown];
SQRLKillAllTestApplications();
}
- (void)addCleanupBlock:(dispatch_block_t)block {
[self.exampleCleanupBlocks addObject:[block copy]];
}
#pragma mark Logging
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths {
NSPipe *outputPipe = [NSPipe pipe];
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
@@ -61,112 +153,24 @@ static NSTask *SQRLTailTaskWithPaths(RACSequence *paths) {
return task;
}
static NSBundle *SQRLTestBundle(void) {
return [NSBundle bundleWithIdentifier:@"com.github.SquirrelTests"];
}
static NSMutableArray *cleanupBlocks = nil;
// The URL to the temporary directory which contains `temporaryDirectoryURL` and
// all copied test data.
static NSURL *baseTemporaryDirectoryURL = nil;
QuickConfigurationBegin(Fixtures)
+ (void)configure:(Configuration *)configuration {
NSURL *appSupportURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
NSAssert(appSupportURL != nil, @"Could not find Application Support folder");
NSArray *folders = @[
@"com.github.Squirrel.TestApplication.ShipIt",
@"com.github.Squirrel.SquirrelTests.ShipIt",
];
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
return @[
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
].rac_sequence;
}];
RACSequence *paths = [URLs map:^(NSURL *URL) {
return URL.path;
}];
NSTask *readShipIt = SQRLTailTaskWithPaths(paths);
[configuration beforeSuite:^{
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
cleanupBlocks = [NSMutableArray array];
for (NSURL *URL in URLs) {
[NSFileManager.defaultManager createDirectoryAtURL:URL.URLByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:NULL];
if (![[NSData data] writeToURL:URL atomically:YES]) {
NSLog(@"Could not touch log file at %@", URL);
}
}
[readShipIt launch];
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
}];
[configuration afterSuite:^{
[readShipIt terminate];
}];
// We want to run any enqueued cleanup blocks after _and before_ each spec,
// in case beforeSuites (for example) use the fixtures.
void (^runCleanupBlocks)(void) = ^{
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
block();
}
[cleanupBlocks removeAllObjects];
};
[configuration beforeEach:^{
runCleanupBlocks();
}];
[configuration afterEach:^{
runCleanupBlocks();
SQRLKillAllTestApplications();
}];
}
QuickConfigurationEnd
@implementation QuickSpec (SQRLFixtures)
#pragma mark Lifecycle
- (void)addCleanupBlock:(dispatch_block_t)block {
[cleanupBlocks addObject:[block copy]];
}
#pragma mark Temporary Directory
- (NSURL *)baseTemporaryDirectoryURL {
if (baseTemporaryDirectoryURL == nil) {
if (_baseTemporaryDirectoryURL == nil) {
NSURL *globalTemporaryDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
_baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
NSError *error = nil;
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", baseTemporaryDirectoryURL, error);
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:_baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
[self addCleanupBlock:^{
[NSFileManager.defaultManager removeItemAtURL:baseTemporaryDirectoryURL error:NULL];
baseTemporaryDirectoryURL = nil;
[NSFileManager.defaultManager removeItemAtURL:_baseTemporaryDirectoryURL error:NULL];
_baseTemporaryDirectoryURL = nil;
}];
}
return baseTemporaryDirectoryURL;
return _baseTemporaryDirectoryURL;
}
- (NSURL *)temporaryDirectoryURL {
@@ -174,7 +178,7 @@ QuickConfigurationEnd
NSError *error = nil;
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
return temporaryDirectoryURL;
}
@@ -184,20 +188,20 @@ QuickConfigurationEnd
- (NSURL *)testApplicationURL {
NSURL *fixtureURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app" isDirectory:YES];
if (![NSFileManager.defaultManager fileExistsAtPath:fixtureURL.path]) {
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
NSError *error = nil;
BOOL success = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:fixtureURL error:&error];
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
[[NSData data] writeToURL:testAppLog atomically:YES];
NSTask *readTestApp = SQRLTailTaskWithPaths([RACSequence return:testAppLog.path]);
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSignal return:testAppLog.path]];
[readTestApp launch];
XCTAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
[readTestApp terminate];
@@ -210,8 +214,8 @@ QuickConfigurationEnd
- (NSBundle *)testApplicationBundle {
NSURL *fixtureURL = self.testApplicationURL;
NSBundle *bundle = [NSBundle bundleWithURL:fixtureURL];
XCTAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
STAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
return bundle;
}
@@ -230,7 +234,7 @@ QuickConfigurationEnd
NSError *error = nil;
NSRunningApplication *app = [NSWorkspace.sharedWorkspace launchApplicationAtURL:self.testApplicationURL options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchAndHide configuration:configuration error:&error];
XCTAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
STAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
[self addCleanupBlock:^{
if (!app.terminated) {
@@ -250,12 +254,12 @@ QuickConfigurationEnd
}
- (NSURL *)createTestApplicationUpdate {
NSURL *originalURL = [SQRLTestBundle() URLForResource:@"TestApplication 2.1" withExtension:@"app"];
XCTAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
NSURL *originalURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication 2.1" withExtension:@"app"];
STAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
NSError *error = nil;
NSURL *updateParentURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:self.baseTemporaryDirectoryURL create:YES error:&error];
XCTAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
STAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
[self addCleanupBlock:^{
[NSFileManager.defaultManager removeItemAtURL:updateParentURL error:NULL];
@@ -263,18 +267,18 @@ QuickConfigurationEnd
NSURL *updateURL = [updateParentURL URLByAppendingPathComponent:originalURL.lastPathComponent isDirectory:YES];
BOOL success = [NSFileManager.defaultManager copyItemAtURL:originalURL toURL:updateURL error:&error];
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
return updateURL;
}
- (id)performWithTestApplicationRequirement:(id (^)(SecRequirementRef requirement))block {
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
SecStaticCodeRef staticCode = NULL;
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
XCTAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
STAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
@onExit {
if (staticCode != NULL) CFRelease(staticCode);
@@ -282,7 +286,7 @@ QuickConfigurationEnd
SecRequirementRef requirement = NULL;
status = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &requirement);
XCTAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
STAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
@onExit {
if (requirement != NULL) CFRelease(requirement);
@@ -292,57 +296,48 @@ QuickConfigurationEnd
}
- (SQRLCodeSignature *)testApplicationSignature {
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
return [[SQRLCodeSignature alloc] initWithRequirement:requirement];
}];
}
NSError *error = nil;
SQRLCodeSignature *signature = [SQRLCodeSignature signatureWithBundle:bundleURL error:&error];
XCTAssertNotNil(signature, @"Error getting signature for bundle at %@: %@", bundleURL, error);
- (NSData *)testApplicationCodeSigningRequirementData {
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
CFDataRef data = NULL;
OSStatus status = SecRequirementCopyData(requirement, kSecCSDefaultFlags, &data);
STAssertTrue(status == noErr, @"Error copying data for requirement %@", requirement);
return signature;
return CFBridgingRelease(data);
}];
}
- (SQRLDirectoryManager *)shipItDirectoryManager {
NSString *identifier = SQRLShipItLauncher.shipItJobLabel;
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:identifier];
XCTAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
STAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
return manager;
}
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote {
if (remote) {
expect(@([[request writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL])).to(beTruthy());
- (void)launchShipIt {
NSError *error = nil;
STAssertTrue([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error], @"Could not launch ShipIt: %@", error);
__block NSError *error = nil;
expect(@([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error])).to(beTruthy());
expect(error).to(beNil());
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
NSError *lookupError;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
expect(stateURL).notTo(beNil());
expect(lookupError).to(beNil());
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
}];
} else {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:self.shipItDirectoryManager.applicationIdentifier];
expect(installer).notTo(beNil());
NSError *installedError = nil;
BOOL installed = [[installer.installUpdateCommand execute:request] asynchronouslyWaitUntilCompleted:&installedError];
expect(@(installed)).to(beTruthy());
expect(installedError).to(beNil());
}
NSError *lookupError = nil;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
STAssertNotNil(stateURL, @"Could not find state URL from %@: %@", self.shipItDirectoryManager, lookupError);
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
}];
}
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL {
NSURL *destinationURL = [self.baseTemporaryDirectoryURL URLByAppendingPathComponent:name];
XCTAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
STAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
NSString *createInvocation;
if (directoryURL == nil) {
@@ -351,15 +346,15 @@ QuickConfigurationEnd
createInvocation = [NSString stringWithFormat:@"hdiutil create '%@' -fs 'HFS+' -volname '%@' -format UDSP -size 10m -srcfolder '%@' -quiet", destinationURL.path, name, directoryURL.path];
}
expect(@(system(createInvocation.UTF8String))).to(equal(@0));
expect(system(createInvocation.UTF8String)).to.equal(0);
NSString *mountInvocation = [NSString stringWithFormat:@"hdiutil attach '%@.sparseimage' -noverify -noautofsck -readwrite -quiet", destinationURL.path];
expect(@(system(mountInvocation.UTF8String))).to(equal(@0));
expect(system(mountInvocation.UTF8String)).to.equal(0);
NSString *path = [NSString stringWithFormat:@"/Volumes/%@", name];
[self addCleanupBlock:^{
NSString *detachInvocation = [NSString stringWithFormat:@"hdiutil detach '%@' -force -quiet", path];
expect(@(system(detachInvocation.UTF8String))).to(equal(@0));
expect(system(detachInvocation.UTF8String)).to.equal(0);
}];
return [NSURL fileURLWithPath:path isDirectory:YES];
+311
View File
@@ -0,0 +1,311 @@
//
// SQRLURLConnectionSpec.m
// Squirrel
//
// Created by Keith Duncan on 27/09/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
#import "SQRLResumableDownload.h"
#import <ReactiveCocoa/EXTScope.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "SQRLHTTPServer.h"
SpecBegin(SQRLURLConnection);
__block SQRLDownloadManager *downloadManager;
beforeAll(^{
downloadManager = SQRLDownloadManager.defaultDownloadManager;
NSError *removeError = nil;
BOOL remove = [[downloadManager
removeAllResumableDownloads]
waitUntilCompleted:&removeError];
expect(remove).to.beTruthy();
expect(removeError).to.beNil();
});
beforeEach(^{
Expecta.asynchronousTestTimeout = 60.;
});
afterAll(^{
Expecta.asynchronousTestTimeout = 1.;
});
describe(@"file scheme URLs", ^{
NSURL *fileLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"test"];
NSData *testContents = [@"test" dataUsingEncoding:NSUTF8StringEncoding];
NSError *error;
BOOL write = [testContents writeToURL:fileLocation options:0 error:&error];
expect(write).to.beTruthy();
expect(error).to.beNil();
NSURLRequest *request = [NSURLRequest requestWithURL:fileLocation];
__block SQRLURLConnection *connection;
beforeEach(^{
connection = [[SQRLURLConnection alloc] initWithRequest:request];
});
void (^testBodyEqualTestContents)(NSData *) = ^ (NSData *body) {
expect(body.length).to.equal(testContents.length);
expect(body).to.equal(testContents);
};
it(@"should retrieve file:// scheme URLs", ^{
NSError *error;
RACTuple *result = [[connection retrieve] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
testBodyEqualTestContents(result[1]);
});
it(@"should download file:// scheme URLs", ^{
NSError *error;
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
NSData *body = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(error).to.beNil();
testBodyEqualTestContents(body);
});
});
static NSData * (^stringTimes)(NSString *, NSUInteger) = ^ (NSString *string, NSUInteger times) {
NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
NSMutableData *data = [NSMutableData dataWithCapacity:(stringData.length + 2) * times];
for (NSUInteger idx = 0; idx < times; idx++) {
[data appendData:stringData];
[data appendData:[NSData dataWithBytes:"\r\n" length:2]];
}
return data;
};
static SQRLHTTPServer * (^newHttpServer)(SQRLTestCase *) = ^ (SQRLTestCase *self) {
SQRLHTTPServer *server = [[SQRLHTTPServer alloc] init];
[self addCleanupBlock:^{
[server invalidate];
}];
return server;
};
it(@"should resume a download", ^{
// Start server
SQRLHTTPServer *server = newHttpServer(self);
NSError *error = nil;
NSURL *baseURL = [server start:&error];
expect(baseURL).notTo.beNil();
expect(error).to.beNil();
error = nil;
// Prepare half response
NSData *firstHalf = stringTimes(@"the quick brown fox", 100);
NSData *secondHalf = stringTimes(@"jumped over the lazy doge", 100);
NSString *ETag = NSProcessInfo.processInfo.globallyUniqueString;
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
NSDictionary *responseHeaders = @{
@"Content-Length": [@(firstHalf.length + secondHalf.length) stringValue],
@"ETag": ETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstHalf);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.beNil();
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.beNil();
return serverResponse;
};
// Issue first request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
request.timeoutInterval = 1.;
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).to.beNil();
expect(error).notTo.beNil();
error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
expect(downloadedData.length).to.equal(firstHalf.length);
expect(downloadedData).to.equal(firstHalf);
expect(error).to.beNil();
// Prepare remainder response
serverResponse = CFHTTPMessageCreateResponse(kCFAllocatorDefault, 206, NULL, kCFHTTPVersion1_1);
responseHeaders = @{
@"Content-Length": [@(secondHalf.length) stringValue],
@"Range": [NSString stringWithFormat:@"%lu-%lu", firstHalf.length, secondHalf.length - 1],
@"ETag": ETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondHalf);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.equal(ETag);
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstHalf.length]));
return serverResponse;
};
// Issue second request
connection = [[SQRLURLConnection alloc] initWithRequest:request];
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
NSMutableData *fullBody = [firstHalf mutableCopy];
[fullBody appendData:secondHalf];
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(downloadedData.length).to.equal(fullBody.length);
expect(downloadedData).to.equal(fullBody);
expect(error).to.beNil();
});
it(@"should not resume downloads for a response with a different ETag", ^{
// Start server
SQRLHTTPServer *server = newHttpServer(self);
NSError *error = nil;
NSURL *baseURL = [server start:&error];
expect(baseURL).notTo.beNil();
expect(error).to.beNil();
error = nil;
// Prepare first response
NSData *firstBody = stringTimes(@"the quick brown fox", 100);
NSString *firstETag = NSProcessInfo.processInfo.globallyUniqueString;
CFHTTPMessageRef serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
NSDictionary *responseHeaders = @{
// Claim the response to be longer so that the connection will timeout
// waiting for the extra bytes
@"Content-Length": [@(firstBody.length + 100) stringValue],
@"ETag": firstETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)firstBody);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.beNil();
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.beNil();
return serverResponse;
};
// Issue first request
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:baseURL];
request.timeoutInterval = 1.;
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:request];
RACTuple *result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).to.beNil();
expect(error).notTo.beNil();
error = nil;
SQRLResumableDownload *download = [[downloadManager downloadForRequest:request] firstOrDefault:nil success:NULL error:&error];
expect(download).notTo.beNil();
expect(error).to.beNil();
NSData *downloadedData = [NSData dataWithContentsOfURL:download.fileURL options:0 error:&error];
expect(downloadedData.length).to.equal(firstBody.length);
expect(downloadedData).to.equal(firstBody);
expect(error).to.beNil();
// Prepare second response
NSData *secondBody = stringTimes(@"jumps over the lazy doge", 100);
NSString *secondETag = NSProcessInfo.processInfo.globallyUniqueString;
serverResponse = (__bridge CFHTTPMessageRef)CFBridgingRelease(CFHTTPMessageCreateResponse(kCFAllocatorDefault, 200, NULL, kCFHTTPVersion1_1));
responseHeaders = @{
@"Content-Length": [@(secondBody.length) stringValue],
@"ETag": secondETag,
};
[responseHeaders enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSString *obj, BOOL *stop) {
CFHTTPMessageSetHeaderFieldValue(serverResponse, (__bridge CFStringRef)key, (__bridge CFStringRef)obj);
}];
CFHTTPMessageSetBody(serverResponse, (__bridge CFDataRef)secondBody);
server.responseBlock = ^ (CFHTTPMessageRef request) {
NSString *ifRange = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("If-Range")));
expect(ifRange).to.equal(firstETag);
NSString *range = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(request, CFSTR("Range")));
expect(range).to.equal(([NSString stringWithFormat:@"%lu-", firstBody.length]));
return serverResponse;
};
// Issue second request
connection = [[SQRLURLConnection alloc] initWithRequest:request];
result = [[connection download:downloadManager] firstOrDefault:nil success:NULL error:&error];
expect(result).notTo.beNil();
expect(error).to.beNil();
NSURL *location = result[1];
downloadedData = [NSData dataWithContentsOfURL:location options:0 error:&error];
expect(downloadedData.length).to.equal(secondBody.length);
expect(downloadedData).to.equal(secondBody);
expect(error).to.beNil();
});
SpecEnd
+20 -21
View File
@@ -6,27 +6,22 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(SQRLUpdateSpec)
SpecBegin(SQRLUpdate)
it(@"should return nil when initialised without a url", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{} error:&error];
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
});
it(@"should return nil when initialised with a url not in URL syntax", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{ @"updateURL": [NSURL URLWithString:@"test"] } error:&error];
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
});
it(@"should validate release name and notes", ^{
@@ -37,20 +32,24 @@ it(@"should validate release name and notes", ^{
@"releaseNotes": [[NSObject alloc] init]
} error:NULL];
expect(update).notTo(beNil());
expect(update.updateURL).to(equal(updateURL));
expect(update.releaseName).to(beNil());
expect(update.releaseNotes).to(beNil());
expect(update).notTo.beNil();
expect(update.updateURL).to.equal(updateURL);
expect(update.releaseName).to.beNil();
expect(update.releaseNotes).to.beNil();
});
it(@"should parse Central style dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:NULL];
expect(update.releaseDate).notTo(beNil());
NSError *error = nil;
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:&error];
expect(update.releaseDate).notTo.beNil();
expect(error).to.beNil();
});
it(@"should parse ISO 8601 dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:NULL];
expect(update.releaseDate).notTo(beNil());
NSError *error = nil;
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:&error];
expect(update.releaseDate).notTo.beNil();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+157 -185
View File
@@ -6,44 +6,38 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLTestUpdate.h"
#import "TestAppConstants.h"
#import "SQRLZipArchiver.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "SQRLDirectoryManager.h"
#import "SQRLURLConnection.h"
#import "SQRLDownloadManager.h"
QuickSpecBegin(SQRLUpdaterSpec)
SpecBegin(SQRLUpdater)
__block NSURL *JSONURL;
void (^writeUpdate)(SQRLUpdate *) = ^(SQRLUpdate *update) {
NSDictionary *updateInfo = [MTLJSONAdapter JSONDictionaryFromModel:update];
expect(updateInfo).notTo(beNil());
expect(updateInfo).notTo.beNil();
NSError *error = nil;
NSData *JSON = [NSJSONSerialization dataWithJSONObject:updateInfo options:NSJSONWritingPrettyPrinted error:&error];
expect(JSON).notTo(beNil());
expect(error).to(beNil());
expect(JSON).notTo.beNil();
expect(error).to.beNil();
BOOL success = [JSON writeToURL:JSONURL options:NSDataWritingAtomic error:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
};
NSURL * (^zipUpdate)(NSURL *) = ^(NSURL *updateURL) {
NSURL *zipFolderURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:self.temporaryDirectoryURL create:YES error:NULL];
expect(zipFolderURL).notTo(beNil());
expect(zipFolderURL).notTo.beNil();
NSURL *zippedUpdateURL = [[zipFolderURL URLByAppendingPathComponent:updateURL.lastPathComponent] URLByAppendingPathExtension:@"zip"];
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zippedUpdateURL fromDirectoryAtURL:updateURL] asynchronouslyWaitUntilCompleted:NULL];
expect(@(success)).to(beTruthy());
expect(success).to.beTruthy();
return zippedUpdateURL;
};
@@ -59,139 +53,96 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
describe(@"updating", ^{
__block NSURL *updateURL;
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo.beNil();
expect(error).to.beNil();
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
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)).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(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)).withTimeout(5).toEventually(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)).withTimeout(5).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(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 = 15;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(@(app.terminated)).withTimeout(delay + 5).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager 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_sequence.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)).withTimeout(5).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect([updateDirectoryURLs toArray]).toEventually(equal(@[]));
});
});
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
beforeEach(^{
before(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
@@ -207,10 +158,10 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(@(result)).to(beFalsy());
expect(error.domain).to(equal(SQRLUpdaterErrorDomain));
expect(@(error.code)).to(equal(@(SQRLUpdaterErrorInvalidServerResponse)));
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
});
it(@"should return an error for non JSON data", ^{
@@ -224,69 +175,90 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(@(result)).to(beFalsy());
expect(error.domain).to(equal(SQRLUpdaterErrorDomain));
expect(@(error.code)).to(equal(@(SQRLUpdaterErrorInvalidServerBody)));
BOOL result = [[updater.checkForUpdatesAction deferred] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerBody);
});
});
static RACSignal * (^stateNotificationListener)(void) = ^ {
return [[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil]
it(@"should clean up resumable downloads after a successful download", ^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication"];
SQRLDownloadManager *downloadManager = [[SQRLDownloadManager alloc] initWithDirectoryManager:directoryManager];
NSError *error = nil;
BOOL remove = [[downloadManager removeAllResumableDownloads] asynchronouslyWaitUntilCompleted:&error];
expect(remove).to.beTruthy();
expect(error).to.beNil();
NSURL *updateApplicationBundle = [self createTestApplicationUpdate];
NSURL *firstDownload = zipUpdate(updateApplicationBundle);
SQRLURLConnection *connection = [[SQRLURLConnection alloc] initWithRequest:[NSURLRequest requestWithURL:firstDownload]];
BOOL download = [[connection download:downloadManager] asynchronouslyWaitUntilCompleted:&error];
expect(download).to.beTruthy();
expect(error).to.beNil();
NSURL *downloadDirectory = [[directoryManager downloadDirectoryURL] firstOrDefault:nil success:NULL error:&error];
expect(downloadDirectory).notTo.beNil();
expect(error).to.beNil();
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
expect(contents.count).to.equal(1);
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
@"updateURL": zipUpdate(updateApplicationBundle),
@"final": @YES,
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:downloadDirectory includingPropertiesForKeys:@[] options:0 error:&error];
if (contents == nil) {
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSFileReadNoSuchFileError);
} else {
expect(contents.count).to.equal(0);
}
});
it(@"should unpack update archives into a .noindex directory", ^{
NSURL *zippedUpdateLocation = zipUpdate([self createTestApplicationUpdate]);
SQRLTestUpdate *update = [[SQRLTestUpdate alloc] initWithDictionary:@{
@keypath(update.updateURL): zippedUpdateLocation,
} error:NULL];
writeUpdate(update);
__block SQRLDownloadedUpdate *downloadedUpdate;
[[[[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil]
map:^(NSNotification *notification) {
return notification.userInfo[SQRLTestAppUpdaterStateKey];
return notification.userInfo[@"update"];
}]
setNameWithFormat:@"stateNotificationListener"];
};
describe(@"state", ^{
it(@"should transition through idle, checking and idle, when there is no update", ^{
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
tryMap:^(NSString *serialisedUpdate, NSError **errorRef) {
NSData *JSONData = [serialisedUpdate dataUsingEncoding:NSUTF8StringEncoding];
return [NSJSONSerialization JSONObjectWithData:JSONData options:0 error:errorRef];
}]
tryMap:^(NSDictionary *JSONDictionary, NSError **errorRef) {
return [MTLJSONAdapter modelOfClass:SQRLDownloadedUpdate.class fromJSONDictionary:JSONDictionary error:errorRef];
}]
subscribeNext:^(SQRLDownloadedUpdate *update) {
downloadedUpdate = update;
}];
NSRunningApplication *testApplication = launchWithEnvironment(nil);
launchWithEnvironment(nil);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateIdle),
];
expect(states).toEventually(equal(expectedStates));
expect(downloadedUpdate).willNot.beNil();
expect(@(testApplication.terminated)).withTimeout(5).toEventually(beTruthy());
});
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
}];
NSError *error;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate([self createTestApplicationUpdate]),
@"final": @YES,
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
writeUpdate(update);
NSRunningApplication *testApplication = launchWithEnvironment(nil);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateDownloadingUpdate),
@(SQRLUpdaterStateAwaitingRelaunch),
];
expect(states).toEventually(equal(expectedStates));
expect(@(testApplication.terminated)).withTimeout(5).toEventually(beTruthy());
});
NSURL *bundleURL = downloadedUpdate.bundle.bundleURL;
expect([bundleURL.path rangeOfString:@".noindex"].location).notTo.equal(NSNotFound);
});
QuickSpecEnd
SpecEnd
+20 -26
View File
@@ -6,44 +6,38 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
QuickSpecBegin(SQRLZipArchiverSpec)
SpecBegin(SQRLZipOperation)
it(@"should extract a zip archive created by the Finder", ^{
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication 2.1.app"];
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should fail to extract a nonexistent zip archive", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:[self.temporaryDirectoryURL URLByAppendingPathComponent:@"foo.zip"] intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(success).to.beFalsy();
NSLog(@"%@", error);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLZipArchiverErrorDomain));
expect(@(error.code)).to(equal(@(SQRLZipArchiverShellTaskFailed)));
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo(equal(0));
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
});
it(@"should create a zip archive readable by itself", ^{
@@ -51,21 +45,21 @@ it(@"should create a zip archive readable by itself", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zipURL fromDirectoryAtURL:self.testApplicationURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@([NSFileManager.defaultManager fileExistsAtPath:zipURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:zipURL.path]).to.beTruthy();
success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app"];
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(success).to.beTruthy();
expect(error).to.beNil();
});
QuickSpecEnd
SpecEnd
+25
View File
@@ -0,0 +1,25 @@
//
// SquirrelTests-Prefix.pch
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-07-21.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SenTestingKit/SenTestingKit.h>
#import <Mantle/Mantle.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import <Squirrel/Squirrel.h>
#define SPT_SUBCLASS SQRLTestCase
#import "Specta.h"
#define EXP_SHORTHAND
#import "Expecta.h"
#import "SQRLTestCase.h"
#endif
-19
View File
@@ -1,19 +0,0 @@
//
// SwiftSpec.swift
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2014-10-14.
// Copyright (c) 2014 GitHub. All rights reserved.
//
import Foundation
import Nimble
import Quick
// Without this, the Swift stdlib won't be linked into the test target (even if
// “Embedded Content Contains Swift Code” is enabled).
class SwiftSpec: QuickSpec {
override func spec() {
expect(true).to(beTruthy())
}
}
+2
View File
@@ -0,0 +1,2 @@
/* Localized versions of Info.plist keys */
-22
View File
@@ -1,22 +0,0 @@
//
// TestAppConstants.h
// Squirrel
//
// Created by Keith Duncan on 05/02/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
// This notification is posted to the distributed notification center when a
// change to the `SQRLUpdater.state` property is observed.
//
// It is posted once on launch with the initial idle state, and then for any
// transition thereafter.
//
// Yhe user info dictionary includes the new state value under the
// `SQRLTestAppUpdaterStateKey` key.
extern NSString * const SQRLTestAppUpdaterStateTransitionNotificationName;
// The state of the updater, an NSNumber.
extern NSString * const SQRLTestAppUpdaterStateKey;
-13
View File
@@ -1,13 +0,0 @@
//
// TestAppConstants.m
// Squirrel
//
// Created by Keith Duncan on 05/02/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "TestAppConstants.h"
NSString * const SQRLTestAppUpdaterStateTransitionNotificationName = @"com.github.Squirrel.TestApplication.state-changed";
NSString * const SQRLTestAppUpdaterStateKey = @"state";
+10 -12
View File
@@ -10,9 +10,9 @@
#import "SQRLDirectoryManager.h"
#import "SQRLShipItLauncher.h"
#import "SQRLTestUpdate.h"
#import "SQRLDownloadedUpdate.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "TestAppConstants.h"
@interface TestAppDelegate ()
@@ -64,12 +64,6 @@
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:request];
self.updater.updateClass = SQRLTestUpdate.class;
[RACObserve(self.updater, state) subscribeNext:^(NSNumber *state) {
NSLog(@"State transition: %@", state);
[NSDistributedNotificationCenter.defaultCenter postNotificationName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil userInfo:@{ SQRLTestAppUpdaterStateKey: state }];
}];
__block NSUInteger updateCheckCount = 1;
[[[[[[[[[[RACSignal
@@ -77,18 +71,22 @@
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
updateCheckCount++;
return [self.updater.checkForUpdatesCommand execute:RACUnit.defaultUnit];
return [self.updater.checkForUpdatesAction deferred];
}]
doNext:^(SQRLDownloadedUpdate *update) {
NSLog(@"Got a candidate update: %@", update);
NSString *serialisedUpdate = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:[MTLJSONAdapter JSONDictionaryFromModel:update] options:0 error:NULL] encoding:NSUTF8StringEncoding];
[NSDistributedNotificationCenter.defaultCenter postNotificationName:@"com.github.Squirrel.TestApplication.updateReceived" object:nil userInfo:@{ @"update": serialisedUpdate }];
}]
// Retry until we get the expected release.
repeat]
skipUntilBlock:^(SQRLDownloadedUpdate *download) {
skipWhile:^ BOOL (SQRLDownloadedUpdate *download) {
SQRLTestUpdate *testUpdate = (id)download.update;
NSAssert([testUpdate isKindOfClass:SQRLTestUpdate.class], @"Unexpected update type: %@", testUpdate);
return testUpdate.final;
return !testUpdate.final;
}]
take:1]
doNext:^(id _) {
@@ -99,12 +97,12 @@
NSLog(@"Error in updater: %@", error);
return [RACSignal empty];
}]
then:^{
concat:[RACSignal defer:^{
NSString *delayString = NSProcessInfo.processInfo.environment[@"SQRLUpdateDelay"];
if (delayString == nil) return [RACSignal empty];
return [[RACSignal interval:delayString.doubleValue onScheduler:RACScheduler.mainThreadScheduler] take:1];
}]
}]]
subscribeCompleted:^{
[NSApp terminate:self];
}];
+2 -9
View File
@@ -10,7 +10,7 @@ config ()
{
# A whitespace-separated list of executables that must be present and locatable.
: ${REQUIRED_TOOLS="xctool"}
export REQUIRED_TOOLS
}
@@ -28,14 +28,7 @@ main ()
check_deps
fi
local submodules=$(git submodule status)
local result=$?
if [ "$result" -ne "0" ]
then
exit $result
fi
local submodules=$(git submodule status 2>/dev/null)
if [ -n "$submodules" ]
then
echo "*** Updating submodules..."
+7 -18
View File
@@ -44,7 +44,7 @@ config ()
# A source-able file with information about the Keychain, containing our
# code-signing certificate.
: ${KEYCHAIN_INFO:=/var/lib/jenkins/config/xcodekeychain}
export XCWORKSPACE
export XCODEPROJ
export BOOTSTRAP
@@ -140,29 +140,18 @@ build_scheme ()
local awkstatus=$?
if [ "$awkstatus" -eq "1" ]
then
# SDK not found, try for iphonesimulator.
sdkflag="-sdk iphonesimulator"
# Determine whether the unit tests will run with iphonesimulator
run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build
awkstatus=$?
if [ "$awkstatus" -ne "0" ]
then
# Unit tests will not run on iphonesimulator.
sdkflag=""
fi
fi
if [ "$awkstatus" -ne "0" ]
then
# Unit tests aren't supported.
action=build
fi
if [ "$awkstatus" -eq "1" ]
then
# Build for iOS.
sdkflag="-sdk iphonesimulator"
fi
run_xctool $sdkflag -scheme "$scheme" $action
}
+5 -9
View File
@@ -95,7 +95,7 @@ module Targets
# Look for an __TEXT,__info_plist section
# This contains the CFBundleIdentifier necessary for signing
`otool -l "#{path}"` =~ /__info_plist/
`otool -l #{path}` =~ /__info_plist/
end
end
@@ -106,10 +106,8 @@ end
module Codesign
class Task
def self.sign(target, identity, timestamp)
arguments = [ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,requirements", "--verbose", target.path ]
arguments.unshift("--timestamp=none") if not timestamp
codesign_with_arguments(arguments)
def self.sign(target, identity)
codesign_with_arguments([ "--sign", identity, "--force", "--preserve-metadata=identifier,entitlements,resource-rules,requirements", "--verbose", target.path ])
end
protected
@@ -139,7 +137,7 @@ unless build_dir && product_name
exit_error("Path to built product not provided")
end
input_path = File.realpath(File.join(build_dir, product_name))
input_path = File.join(build_dir, product_name)
unless File.exists?(input_path)
exit_error("Built product doesn't exist at path provided")
end
@@ -170,7 +168,6 @@ while paths_to_search.size > 0
Dir.foreach(search_path) do |current_directory_entry|
next if current_directory_entry.start_with?(".")
File.delete(File.join(search_path, current_directory_entry)) if current_directory_entry.end_with?(".cstemp")
target = match_path(File.join(search_path, current_directory_entry))
next if target.nil?
@@ -196,8 +193,7 @@ targets = targets.sort_by do |target|
end
targets.reverse!
use_timestamp = ENV["CONFIGURATION"] == "Release"
targets.each do |target|
puts "=> #{target.path}"
Codesign::Task.sign(target, codesign_identity, use_timestamp)
Codesign::Task.sign(target, codesign_identity)
end
+3 -1
View File
@@ -2,8 +2,10 @@ BEGIN {
FS = "\n";
}
/Schemes:/ {
/Targets:/ {
while (getline && $0 != "") {
if ($0 ~ /Test/) continue;
sub(/^ +/, "");
print "'" $0 "'";
}