Author SHA1 Message Date
Keith Duncan 0828897f1b 🍩 pass the request path around 2014-09-02 17:08:50 +02:00
Keith Duncan ff31a5fcdd Replace the installer with an XPC version
Doesn’t read install requests from the file system, also makes the installer installable once rather than once per install request.
2014-09-01 22:28:31 +02:00
Keith Duncan 016cd25fda Update docs 2014-09-01 22:23:13 +02:00
Keith Duncan afebefc78a Remove the watcher 2014-08-05 21:46:50 +01:00
Keith Duncan 3c7916e02e More building++ 2014-08-05 21:20:47 +01:00
Keith Duncan 8d6063d9a9 Fix missed conflict in merge 2014-08-05 21:17:50 +01:00
Keith Duncan 24c118fa1e xctest? 2014-08-05 21:17:48 +01:00
Keith Duncan ff7b9de34f Remove the watcher 2014-08-05 21:17:19 +01:00
Keith Duncan 5994cf9584 Merge remote-tracking branch 'origin/master' into dont-link-appkit
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLShipItRequest.m
	SquirrelTests/SQRLTestCase.m
2014-08-05 21:05:49 +01:00
Josh Abernathy d0b18ef3a4 Merge pull request #123 from Squirrel/zero-warnings
Zero warnings
2014-07-16 16:46:43 -07:00
Matt Diephouse b3a0f3d92d ++OHHTTPStubs 2014-07-16 16:16:00 -07:00
Matt Diephouse 3a7ec986bc Update upgrade check 2014-07-15 16:34:14 -07:00
Matt Diephouse 71ab28ba2b Automatically select architectures 2014-07-15 16:34:09 -07:00
Matt Diephouse fd804f1fe3 ++MTL 2014-07-15 16:32:05 -07:00
Matt Diephouse f70a22bf63 ++RAC 2014-07-09 17:05:17 -04:00
Josh Abernathy c79655b4ed Merge pull request #122 from Squirrel/xctest
++MTL, ++RAC, and move to XCTest
2014-07-08 10:34:51 -04:00
Matt Diephouse 1db8f6d2cb T A B S 2014-07-07 23:42:55 -04:00
Matt Diephouse 5d79ef8157 ++RAC and move to XCTest 2014-07-07 23:40:00 -04:00
Matt Diephouse 64d1fd3e16 ++MTL 2014-07-07 14:58:42 -04:00
Justin Spahr-Summers 6e2819c643 ++RAC to fix header import 2014-06-23 12:00:20 -07:00
Matt Diephouse 3f2ba95276 ++RAC 2014-06-23 12:33:59 -04:00
Justin Spahr-Summers b209180b29 ++RAC 2014-05-06 10:36:01 -07:00
Justin Spahr-Summers b407f94610 Merge remote-tracking branch 'origin/flatten-dependencies'
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
2014-04-15 10:49:43 -07:00
Justin Spahr-Summers 216bb7c14e Merge pull request #116 from Squirrel/catch-decoding-exception
Update the request serialisation to avoid encoding class names
2014-04-15 10:44:19 -07:00
Keith Duncan ecd0a90a22 Remove error branch
Mantle handles this conditional and returns an error.
2014-04-15 12:05:41 +01:00
Keith Duncan 7c5b3250a2 Add NSLocalizedString to string constant 2014-04-14 11:37:19 +01:00
Keith Duncan 34f4db9151 Replace the mutable dictionaries 2014-04-14 11:18:34 +01:00
Keith Duncan c609d67bf5 Replace the non-<key> prefixed value transformer method 2014-04-14 11:16:50 +01:00
Keith Duncan df352a1797 Change the request to serialise as JSON 2014-04-03 14:58:20 +01:00
Rob Rix 9df71c5e0f Find dependencies in archive builds too. 2014-03-27 15:10:20 -04:00
Rob Rix a783128701 Search for headers in . 2014-03-27 13:45:50 -04:00
Rob Rix 74c652cb30 Update RAC to a version with better paths for .a headers. 2014-03-27 13:45:24 -04:00
Rob Rix 2c4ef50bde Remove bogus library search path for ShipIt. 2014-03-27 13:15:10 -04:00
Rob Rix 92bc6103b5 Link TestApplication against RAC & Mantle. 2014-03-27 10:42:58 -04:00
Rob Rix ba4fcc3e20 Link tests against the frameworks again. 2014-03-27 10:38:09 -04:00
Rob Rix cc202d0648 RAC & Mantle static libs are found relative to built products dir. 2014-03-27 10:35:57 -04:00
Rob Rix ffc64bbb70 Copy Mantle & RAC into TestApplication. 2014-03-27 10:33:53 -04:00
Rob Rix bc506173ab Bump schemes. 2014-03-27 10:08:49 -04:00
Rob Rix 29ac9bf8b6 Document the workspace. 2014-03-26 16:57:35 -04:00
Rob Rix 9c8117c12d Note the need to import dependencies. 2014-03-26 16:57:30 -04:00
Rob Rix 380d42f454 Pull dependencies from the workspace. 2014-03-26 16:57:10 -04:00
Rob Rix f1eb83674f Use the right OHHTTPStubs project. 2014-03-26 16:56:59 -04:00
Rob Rix 9b55db7077 Add a workspace for standalone development. 2014-03-26 16:56:40 -04:00
Rob Rix 088c06087f Bump dependencies. 2014-03-26 16:56:35 -04:00
Alan Rogers 763efbb0c2 Merge pull request #114 from Squirrel/flatten-dependencies
Flatten dependencies
2014-03-27 00:08:46 +11:00
Rob Rix 8548dbcf3f Bump deps. 2014-03-25 20:18:39 -04:00
Rob Rix 5b4bfa8a12 Document the workspace. 2014-03-25 16:13:01 -04:00
Rob Rix 4ac47f3feb Bump deps. 2014-03-25 16:12:53 -04:00
Rob Rix 1fcfee2d62 Bump dependencies. 2014-03-24 15:53:29 -04:00
Rob Rix ca1b58764d Scheme version bump. 2014-03-24 12:48:30 -04:00
Rob Rix 9ace4fab90 Note the need to import dependencies. 2014-03-24 12:48:19 -04:00
Keith Duncan 37602d63cf Catch NSInvalidUnarchiveOperationException when decoding
If the archive is of a class not present, this exception is thrown.

Ideally we’d be able to use `NSSecureCoding` to abort decoding before running the decode initialiser of any rogue classes.
2014-03-24 11:44:25 +00:00
Rob Rix 72b2c6b351 Pull dependencies from the workspace. 2014-03-20 18:29:08 -04:00
Rob Rix 01d9c026a5 Bump dependencies. 2014-03-20 18:27:25 -04:00
Rob Rix d8aea5a8be Use the right OHHTTPStubs project. 2014-03-20 18:27:12 -04:00
Rob Rix 45c3dffa98 Add a workspace for standalone development. 2014-03-20 18:00:29 -04:00
Rob Rix e33a715f0a Merge pull request #113 from Squirrel/xcconfigs-bump
Bump RAC and Mantle for xcconfigs updates.
2014-03-11 09:31:27 -04:00
Alan Rogers 4cae85f5ec ++RAC for xcconfigs bump 2014-03-11 17:47:29 +11:00
Alan Rogers 24c38be4f7 ++OHHTTPStubs 2014-03-11 17:46:42 +11:00
Alan Rogers c7ae200e0d ++Mantle 2014-03-11 17:46:04 +11:00
Justin Spahr-Summers e98559dcab ++RAC for +rac_sendAsynchronousRequest: fix 2014-02-26 09:42:48 -08:00
Keith Duncan 518ee666a3 Restrict the permissions of the wait files directory 2014-02-19 14:46:17 +00:00
Keith Duncan 73cc6802d3 Clarify the installer args 2014-02-19 13:58:26 +00:00
Keith Duncan 1403a7ad81 Make the watcher job dictionary method return a signal
Fix the signal names for the job dictionary methods.
Extract the watcher job label into a method.
2014-02-19 13:57:16 +00:00
Keith Duncan 24d9ef1f86 Remove the debug launchd option 2014-02-18 13:51:52 +00:00
Keith Duncan 51bd593666 Remove the domain/authorisation tuple 2014-02-18 13:12:27 +00:00
Keith Duncan 44a471b903 Concat the submit job signals inline 2014-02-18 13:12:13 +00:00
Keith Duncan 6e2f4faa3f Style++ 2014-02-18 13:06:33 +00:00
Keith Duncan 43fd18e4c9 Less URL->path conversions 2014-02-18 13:06:02 +00:00
Keith Duncan d08804ddce Less URL->path conversions 2014-02-18 13:01:04 +00:00
Keith Duncan 39a47ac14f s/command/executableName 2014-02-18 12:58:35 +00:00
Keith Duncan 49148013dc Request cannot be nil 2014-02-18 12:57:51 +00:00
Keith Duncan 15c80b1cbf Better docs for the send request method 2014-02-18 12:57:14 +00:00
Keith Duncan f89762550f Rename the connection initialiser 2014-02-18 12:54:59 +00:00
Justin Spahr-Summers ca100f1e09 ++RAC to v2.2.4 2014-02-17 11:23:07 -08:00
Keith Duncan b224b30a27 s/[[:space:]]/[[:tab:]] 2014-02-14 19:12:08 +00:00
Keith Duncan bfa660b5bc Add docs on the multiprocess topology to SQRLShipItConnection 2014-02-14 19:03:00 +00:00
Keith Duncan 3ed1dc26fa Add docs for the installer args 2014-02-12 16:34:23 +00:00
Keith Duncan 442e51b89d Change the watcher args to be URLs 2014-02-12 16:34:04 +00:00
Keith Duncan a8b6a9f262 Condense the job dictionary creation 2014-02-12 16:26:34 +00:00
Keith Duncan 05019c8e82 New file listener API
Now with 💯% more error checking
2014-02-12 16:13:45 +00:00
Keith Duncan d569a6524e Document the shipit-watcher arguments 2014-02-12 12:57:29 +00:00
Keith Duncan 5c64d501f4 Style++ 2014-02-12 12:43:25 +00:00
Keith Duncan ee8c781284 Fix signal names 2014-02-06 21:22:38 +00:00
Keith Duncan d8b1c4b6ca Merge remote-tracking branch 'origin/master' into dont-link-appkit
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
	Squirrel/SQRLInstaller.m
	Squirrel/SQRLShipItState.h
	Squirrel/SQRLShipItState.m
	Squirrel/SQRLUpdater.m
	Squirrel/ShipIt-main.m
	SquirrelTests/SQRLInstallerSpec.m
	SquirrelTests/SQRLShipItStateSpec.m
	SquirrelTests/SQRLTestCase.h
	SquirrelTests/SQRLTestCase.m
2014-02-06 21:22:15 +00:00
Justin Spahr-Summers e448634a90 Merge pull request #76 from Squirrel/verify-target-requirement
Improve bundle replacement security by using the target designated requirement
2014-02-06 11:36:10 -08:00
Keith Duncan 3e7d0fcb61 Change the clean up test to verify no dirs
Change the `to` to a `will` because the cleanup happens after the `self.testApplicationBundleVersion` expect passes.
2014-02-06 19:16:50 +00:00
Keith Duncan e344702928 Merge remote-tracking branch 'origin/master' into verify-target-requirement
Conflicts:
	Squirrel.xcodeproj/project.pbxproj

The empty updates test is broken due to changes on this branch where the update is copied not moved, will update now to have shipit also remove the update when it completes.
2014-02-06 18:51:51 +00:00
Justin Spahr-Summers 1bdd03348f Merge pull request #98 from Squirrel/store-state-in-defaults
Change ShipIt to store the in-flight install state in defaults
2014-02-05 09:33:34 -08:00
Justin Spahr-Summers b482f33d2c Merge pull request #106 from Squirrel/status-notification
Adds the state property back to `SQRLUpdater`
2014-02-05 09:30:28 -08:00
Keith Duncan 2c86404be6 Remove return type 2014-02-05 16:34:01 +00:00
Keith Duncan 538e6a3cc7 Add constants file for the test app notification and key 2014-02-05 16:32:32 +00:00
Keith Duncan f84f75ed8b Style++ 2014-02-05 16:24:52 +00:00
Keith Duncan aa9c607cba Shore up the state transition tests 2014-02-05 16:24:28 +00:00
Keith Duncan d26a1743ea Switch back to doCompleted: 2014-02-05 16:12:47 +00:00
Keith Duncan ea29a85f91 Style around the performInstall:remote: method 2014-02-05 13:32:30 +00:00
Keith Duncan 07d861ee77 Hoist operators out of a flattenMap and chain 2014-02-05 13:32:08 +00:00
Keith Duncan a55be876af Document nil/non-nil parameters 2014-02-05 13:21:18 +00:00
Keith Duncan 244735cd93 Remove deferred array of urls to clean up
Replace with `concat:[RACSignal return:]]`
2014-02-05 13:20:08 +00:00
Keith Duncan e1cc5c9785 Make the ownedBundle property atomic 2014-02-05 13:09:43 +00:00
Keith Duncan f976e3ef41 Style++ 2014-02-05 13:09:38 +00:00
Keith Duncan 355730583b Pass the directory manager in to installRequest() 2014-02-05 12:17:27 +00:00
Keith Duncan 2701b1ed3a Log errors when aborting prior to install 2014-02-05 12:15:57 +00:00
Keith Duncan 3e96285115 Actually working this time 2014-02-03 13:08:43 +00:00
Keith Duncan 4e4d1dd107 Move the downloading and awaiting relaunch states 2014-02-03 12:56:37 +00:00
Alan Rogers c7915326d9 Merge pull request #109 from Squirrel/docs-update
Update the docs and clarify the request provided and response expectations
2014-01-28 15:02:19 -08:00
Keith Duncan e10877e30e Clarify that the request is coming from the app 2014-01-28 22:41:26 +00:00
Keith Duncan 92f98ac82c Update the Configuration example to include version 2014-01-28 22:39:45 +00:00
Keith Duncan a079880b18 “is expected to” -> “should” 2014-01-28 19:53:34 +00:00
Keith Duncan 2b5b52a551 Present tense++ 2014-01-28 19:52:09 +00:00
Keith Duncan ce0d740bc5 Cross link the request and response sections
Re-add that Squirrel adds `Accept: application/json` to the request.
2014-01-28 19:50:17 +00:00
Keith Duncan 0939c6d331 Add an Update Request section, restucture Update JSON Format under Server Support 2014-01-28 19:34:21 +00:00
Keith Duncan d8d5021d7f Remove a curly apostrophe 2014-01-28 19:33:57 +00:00
Keith Duncan 6e6fc6b135 Newlines++, 80 char wrapping 2014-01-28 19:33:44 +00:00
Keith Duncan be9e933570 Punctuate a sentence. 2014-01-28 19:08:14 +00:00
Keith Duncan 1d1e8fbd7b Run the run loop to ensure the distributed notifications are received 2014-01-27 23:48:34 +00:00
Keith Duncan 0370434afa Move the status tests to use the remote updater and distributed notifications 2014-01-27 23:48:20 +00:00
Keith Duncan 9af227eb87 Fix returning to idle when there’s an error 2014-01-27 23:41:46 +00:00
Keith Duncan 48b99d7af3 Add tests for the state transitions 2014-01-27 19:06:54 +00:00
Keith Duncan ca89eba391 Add state back again 2014-01-27 16:04:32 +00:00
Keith Duncan 474efc80c3 Parenthesis-- 2014-01-27 14:31:01 +00:00
Keith Duncan 35dbedaa90 Replace -tryMap: -> +defer: 2014-01-27 14:25:48 +00:00
Keith Duncan 8f4eaca994 Update SQRLShipItRequest initialiser documentation 2014-01-27 13:07:13 +00:00
Keith Duncan 38950afee0 Rename codeSignatureForURL: -> codeSignatureForBundleAtURL: 2014-01-27 13:02:47 +00:00
Keith Duncan d0362eea95 Assertion++ 2014-01-27 13:01:41 +00:00
Keith Duncan d7e8f063b5 Tweak error description 2014-01-27 13:01:25 +00:00
Keith Duncan 6c744d6147 Fix signal name 2014-01-27 13:00:56 +00:00
Keith Duncan 5663b6f1da Better signal chaining
Also log errors that occur removing owned bundle locations.
2014-01-27 12:55:36 +00:00
Keith Duncan 59612461ff Document the ownedBundle property 2014-01-27 12:29:12 +00:00
Keith Duncan c443572a72 Style++ 2014-01-27 12:25:43 +00:00
Keith Duncan 5845e018fb Temporaries--, use -zipWith: rather than +zip: 2014-01-27 12:25:10 +00:00
Keith Duncan 9ece097efb Documentation++ 2014-01-27 12:19:28 +00:00
Keith Duncan 220fc8b0ba Add conditional when setting the owned bundle to nil 2014-01-27 12:16:12 +00:00
Keith Duncan 2cf1badb11 Clarify documentation 2014-01-27 12:11:35 +00:00
Keith Duncan 1b66944219 🇺🇸++ 2014-01-27 12:10:31 +00:00
Keith Duncan 1f4a2c69fa Document the private keys 2014-01-27 12:06:14 +00:00
Keith Duncan 3fb7cf58d1 Merge branch 'verify-target-requirement' into store-state-in-defaults 2014-01-20 16:56:46 -08:00
Keith Duncan 98ac354419 Merge branch 'master' into verify-target-requirement 2014-01-20 16:56:13 -08:00
Keith Duncan 0f35931a69 Merge branch 'verify-target-requirement' into store-state-in-defaults
Conflicts:
	SquirrelTests/SQRLInstallerSpec.m
2014-01-20 16:55:24 -08:00
Keith Duncan 3f0d7558e1 Merge remote-tracking branch 'origin/master' into store-state-in-defaults
Conflicts:
	Squirrel.xcodeproj/project.pbxproj
2014-01-20 16:45:54 -08:00
Keith Duncan 928f557782 Remove todo, this is fixed in dont-link-appkit https://github.com/Squirrel/Squirrel.Mac/pull/102 2014-01-20 16:43:25 -08:00
Keith Duncan 3582cdec0b Fix the signal tests to use the new process name 2014-01-20 16:26:39 -08:00
Keith Duncan ef169d19e9 Fix task launching on 10.7 2014-01-20 16:19:19 -08:00
Keith Duncan 582593dca7 Fix 10.7 support 2014-01-20 16:14:24 -08:00
Keith Duncan ba6df2df14 Fix argument docs 2014-01-20 16:09:13 -08:00
Keith Duncan 65463d7612 Rename signal 2014-01-20 16:07:54 -08:00
Keith Duncan 1f38d749ea Use the job label when removing and adding 2014-01-20 16:00:14 -08:00
Keith Duncan c4871aa371 Add launch support using LaunchServices directly 2014-01-20 15:18:15 -08:00
Keith Duncan f067d6b778 Zip++ 2014-01-20 15:18:11 -08:00
Keith Duncan 2c10c116e9 Clean up the wait file when the installer completes 2014-01-20 12:53:05 -08:00
Keith Duncan cfc2d55ca9 Write the state before submitting the jobs 2014-01-20 12:40:22 -08:00
Keith Duncan daaf62645e Don’t pass the label to the watcher 2014-01-20 12:40:15 -08:00
Keith Duncan f34c02b79b Run watcher on launch 2014-01-20 12:29:23 -08:00
Keith Duncan 5d5f5eaa12 Make submitJob:... a cold signal 2014-01-20 12:05:01 -08:00
Keith Duncan 40e5ac130e Fix types issue, the authorization in the tuple is a signal 2014-01-20 11:57:18 -08:00
Keith Duncan e449f18bd3 Tweak test description 2014-01-20 11:56:54 -08:00
Keith Duncan 6d4f8a731b Xcode updating schemes 2014-01-17 17:20:16 -08:00
Keith Duncan 9debcdcd57 Update to the new NSURL argument for state read/write 2014-01-17 17:19:57 -08:00
Keith Duncan 4d489e5add Style++ 2014-01-17 17:19:37 -08:00
Keith Duncan 60f4089f6f Use the new file watcher rather than termination listener 2014-01-17 17:19:13 -08:00
Keith Duncan 33d2c31db9 Switch to using the NSURL argument type 2014-01-17 17:18:46 -08:00
Keith Duncan 267cd56bd6 Add file listener class
Parallels the termination listener
2014-01-17 17:13:22 -08:00
Keith Duncan 2a4147f3b1 Make ShipIt state take plain NSURL arguments
Get the watcher job to launch too, still needs to listen for its file in the installer.
2014-01-17 12:30:12 -08:00
Keith Duncan fb991f348b Rename job submit method 2014-01-17 11:37:04 -08:00
Keith Duncan 839f4a9708 Remove Mach service, this isn’t used 2014-01-17 11:31:37 -08:00
Keith Duncan e142e1bef0 Implement the new watcher process 2014-01-15 13:48:40 -08:00
Keith Duncan 14892783cd Start a new target for the watcher 2014-01-15 10:25:42 -08:00
Keith Duncan 88c65394c1 Implement a separate watch command
This is so that we can split the waiting into running in the user launchd domain, and the install still runs in the root domain.
2014-01-15 10:16:17 -08:00
Keith Duncan afc0ad0b20 Don’t use NSRunningApplication in SQRLDirectoryManager
This is AppKit API and `SQRLDirectoryManager` should be Foundation only.
2014-01-15 10:11:21 -08:00
Keith Duncan 0c533da4d7 Start launch logic in SQRLShipItConnection 2014-01-12 20:45:17 +00:00
Keith Duncan a5f8798ce8 Remove the Cocoa linkage from ShipIt, link Foundation instead 2014-01-12 20:20:50 +00:00
Keith Duncan ab52a12630 Remove launch handing from ShipIt-main
This will be handled external to ShipIt which can run as root and therefore should not fork or comminicate with LaunchServices.
2014-01-12 20:18:17 +00:00
Keith Duncan 954de5d480 Move SQRLTestCase to the top of the group 2014-01-12 20:17:31 +00:00
Keith Duncan df71c4d256 Renme the launchShipIt method 2014-01-12 20:17:15 +00:00
Keith Duncan d73df99d57 Remove un-necessary +defer 2014-01-12 20:16:54 +00:00
Keith Duncan bfc2652481 Pass the SQRLShipItState to SQRLShipItConnection for writing
The launched ShipIt is now a function of the written request.
2014-01-12 19:56:24 +00:00
Keith Duncan 90f31dd0f8 Use a connection instance to send the request
Whether to launch the target is now an argument, this needs to be handled by this process not the aux. process which may be root.
2014-01-12 19:41:20 +00:00
Keith Duncan 470338574f Rename SQRLShipItLauncher to SQRLShipItConnection 2014-01-12 19:34:08 +00:00
Keith Duncan 1de5029226 Clear the resume count on _complete_ not _next_ 2014-01-09 21:49:38 +00:00
Keith Duncan 14f6b2f840 Add a name to the abort signal 2014-01-09 21:49:38 +00:00
Keith Duncan 807d812011 Move the clearing of the acquired bundle to the same level as acquire 2014-01-09 21:49:38 +00:00
Keith Duncan d9dcd6984b Fix non-remote installs to use ShipIt’s application identifier 2014-01-09 21:49:38 +00:00
Keith Duncan 6dc7e9b199 Fix to another volume test 2014-01-09 21:49:38 +00:00
Keith Duncan d3a53088b7 Style wibble 2014-01-09 21:49:38 +00:00
Keith Duncan 337a10604b Replace SQRLShipItState with SQRLShipItRequest
This avoids the state parameters being passed to the root installer, and forces the root installer to store its state in a non-user accessible location.

This reimplements the the installer to instead of tracking the state of the update request, only track the state of the bundle that is being replaced.

Each time we launch we can prepare a fresh update.

This handles the problem where on launch we don’t know if we crashed/failed on a prior launch and are resuming an update, or have been given a new update request to handle. This means we don’t have to duplicate the incoming request into secure storage, because the incoming request doesn’t contain any of the installer state.
2014-01-09 21:42:01 +00:00
Keith Duncan ddd5c3d1e2 Move away from SQRLShipItState
Store the installation attempts in the preferences, needs to be cleared
2014-01-08 15:08:05 +00:00
Keith Duncan bc382530c8 Ditch the state, add SQRLShipItRequest which contains the bare minimum
This is the client sent structure and has no ShipIt internals included, all ShipIt state will be stored in preferences.

The only important state is whether targetURL is still present, or if it’s been moved out of the way, and the number of installation attempts made so far.
2014-01-08 14:46:13 +00:00
Keith Duncan f7000a05cd Use file coordination for the state
Ensures we have mutual exclusion when reading the state, because other processes may be writing an update too.
2014-01-08 14:00:38 +00:00
Keith Duncan ec8f376fe9 Fix method signature in tests 2014-01-08 13:19:59 +00:00
Keith Duncan 8133dd8b4b Switch between the defaults state and disk state 2014-01-08 13:16:38 +00:00
Keith Duncan d1e38b4c83 Use value from block 2014-01-08 13:03:21 +00:00
Keith Duncan 11e55547cd Fix docs 2014-01-08 13:03:03 +00:00
Keith Duncan c1410d93b6 whitespace-- 2014-01-07 22:12:15 +00:00
Keith Duncan 2b33ac168c Update docs to reflect that the root prefs aren’t opposed to shipit state
ShipIt state is now in the prefs too.
2014-01-07 22:11:47 +00:00
Keith Duncan 20ca9a0c7c Fix SQRLInstallerSpec reading the installer state from defaults
Previously it read from disk, modifications to the state aren’t stored there anymore.
2014-01-07 22:00:28 +00:00
Keith Duncan 2ddf719dc6 Update tests to new API 2014-01-07 22:00:23 +00:00
Keith Duncan 33edaff22e SQRLInstaller now saves the state to the defaults.
It knows nothing of the directory manager.
2014-01-07 22:00:16 +00:00
Keith Duncan ff1b155071 Move the installer to take appId and state key 2014-01-07 22:00:10 +00:00
Keith Duncan 1739d0fa24 Fix method signature for installer init 2014-01-07 22:00:05 +00:00
Keith Duncan 07a3ab75da Fix TestApplication launching 2014-01-07 22:00:01 +00:00
Keith Duncan ad29c492db Add tests for new defaults serialisation too 2014-01-07 21:59:55 +00:00
Keith Duncan b340b356b3 Add support for reading/writing state to user defaults 2014-01-07 21:59:41 +00:00
Keith Duncan e6d344c40a Merge pull request #85 from Squirrel/protect-against-symlink-attacks
Protect against symlink attacks
2014-01-07 03:50:29 -08:00
Justin Spahr-Summers 7a13b894ed Verify newly-owned bundles after moving them into place 2013-11-22 12:29:17 -08:00
Keith Duncan 3d95f4276e Merge pull request #87 from Squirrel/target-requirement-tests
Test SQRLCodeSignature with a bundle & changed permissions during install
2013-11-22 06:23:39 -08:00
Justin Spahr-Summers e17262ac51 Test for permissions changes during installation 2013-11-21 18:52:42 -08:00
Justin Spahr-Summers 4b7cd53fb4 Fix name of SQRLCodeSignature spec 2013-11-21 18:34:09 -08:00
Justin Spahr-Summers 8eaa963033 Hide -initWithRequirement: to test +signatureWithBundle: 2013-11-21 18:33:39 -08:00
Justin Spahr-Summers 025a5e30c0 No need to take ownership of the target bundle
If it's writable by others, then there's already a vulnerability there,
so changing its permissions won't really protect us.
2013-11-20 09:41:56 -08:00
Justin Spahr-Summers 015588055a Clarify that -takeOwnership… works recursively 2013-11-20 09:40:47 -08:00
Justin Spahr-Summers 002aa3c177 Remove obsolete copyfile.h import 2013-11-20 09:40:09 -08:00
Justin Spahr-Summers e4d7285e6c Remove debug logging 2013-11-19 15:14:14 -08:00
Justin Spahr-Summers 93ba301b74 Unfocus spec 2013-11-19 15:12:50 -08:00
Justin Spahr-Summers 7f2dc8f0e4 Refactor backup restoration tests to write ShipIt prefs 2013-11-19 15:12:27 -08:00
Justin Spahr-Summers 60232a6565 Try the backup if final in-place verification fails 2013-11-19 14:55:50 -08:00
Justin Spahr-Summers 49a13566fb Combine steps for preparing the update bundle 2013-11-19 14:55:33 -08:00
Justin Spahr-Summers 42f7578389 Verify the owned update once more before installing 2013-11-19 14:50:40 -08:00
Justin Spahr-Summers ce809e6a44 Rename URL keys to "owned…BundleURL" for clarity 2013-11-19 14:43:28 -08:00
Justin Spahr-Summers ffac048d73 Missed a spot 2013-11-19 14:38:30 -08:00
Justin Spahr-Summers 9fae88e8d1 Always read the target's code signature directly
Don’t ever depend on the signature passed in.
2013-11-19 14:30:50 -08:00
Justin Spahr-Summers b69d560259 Fix SQRLInstallerSpec sending too many SIGKILLs 2013-11-19 14:08:40 -08:00
Justin Spahr-Summers d227e92f3e Take ownership before verifying code signatures
Also verify backups before restoring them.
2013-11-19 14:06:10 -08:00
Justin Spahr-Summers e174e130be Move the updateBundle to a private location
Also, take ownership of the updateBundle and backupBundle by changing
their permissions.
2013-11-19 13:36:29 -08:00
Justin Spahr-Summers 28e00eed97 Merge branch 'verify-target-requirement' into protect-against-symlink-attacks 2013-11-19 11:28:04 -08:00
Justin Spahr-Summers 9db94ba955 Merge branch 'master' into verify-target-requirement 2013-11-19 11:27:59 -08:00
Justin Spahr-Summers 9b7e0788e4 Store the backup bundle URL in preferences instead
This avoids attacks on the backup bundle that rely upon manipulating
SQRLShipItState on disk.
2013-11-19 11:08:17 -08:00
Justin Spahr-Summers 38a30e40a9 Add methods for reading/writing URLs in preferences 2013-11-19 11:08:17 -08:00
Justin Spahr-Summers 869b47a1b5 Move ShipIt group up in the project navigator 2013-11-19 10:40:51 -08:00
Justin Spahr-Summers 4782828a4d Break down file security methods and rename them 2013-11-18 18:22:55 -08:00
Justin Spahr-Summers 123e7fbef9 Fix outdated enum key in documentation 2013-11-18 17:55:20 -08:00
Justin Spahr-Summers 8ff5514bf1 ++style 2013-11-18 17:48:26 -08:00
Justin Spahr-Summers e6d4763c47 Style, documentation, and RAC fixes 2013-11-18 17:42:19 -08:00
Justin Spahr-Summers ba6e7abe12 Merge remote-tracking branch 'origin/master' into verify-target-requirement
Conflicts:
	Squirrel/SQRLInstaller.m
	Squirrel/SQRLShipItState.h
2013-11-18 17:18:35 -08:00
Keith Duncan 96772f8a07 Remove focused test 2013-10-31 13:07:38 +00:00
Keith Duncan d86a513605 Clarify the SQRLInstallerStateUpdatingPermissions docs 2013-10-31 13:06:54 +00:00
Keith Duncan 12a33b5b4c Reorder the state docs, clarify the SQRLInstallerStateVerifyingRequirement docs 2013-10-31 13:04:00 +00:00
Keith Duncan 6f74aa1698 Move target application code signature creation into SQRLCodeSignature
Clarify in the docs that the bundle verified is code sign verified and checked to meet the requirement given.
2013-10-31 13:01:26 +00:00
Keith Duncan 4e1232e63b Replace zip:] reduce:] with zip:reduce: 2013-10-31 12:42:32 +00:00
Keith Duncan f50aaf807a Fix style 2013-10-31 12:41:44 +00:00
Keith Duncan 63f3b3eaae Break enumeration down into a signal of signals
Change `-updateFileSecurity:` to return a signal, this contains the get, modify and set of the file security and captures all the errors.

I still have the modification of the `CFFileSecurity` in a separate method because it is non error byref to avoid duplicating the error handling at each `return NO` call site.
2013-10-31 12:39:57 +00:00
Keith Duncan a6845ce01a Use fast enumeration 2013-10-31 12:23:09 +00:00
Keith Duncan e9fdbc237e Track whether an enumeration error occurs
Don’t send completed when we’ve sent an error
2013-10-31 12:23:09 +00:00
Keith Duncan e123cfa3fe Use RACScheduler in place of NSOperationQueue 2013-10-31 12:23:09 +00:00
Keith Duncan affe3d3792 Rename the update permissions method
Having two verbs "change" and "update" made it read oddly, "updateBundle" is a noun so I think it’s better.
2013-10-31 12:23:04 +00:00
Keith Duncan 0b593abc10 Tidy up return statements 2013-10-30 20:08:13 +00:00
Keith Duncan dc43429a25 Add +[SQRLInstaller initialInstallerState]
This controls where the fresh install starts from, I quibbled about adding this to `SQRLShipItState` instead but thought it belonged in the installer where the order is defined.
Use the new initial state method in ShipIt-main and the in process install test.
2013-10-30 20:03:54 +00:00
Keith Duncan ed03d2c5aa Merge branch 'master' into verify-target-requirement 2013-10-30 20:03:36 +00:00
Keith Duncan 624b6b88db Add verifying of the target bundle's designated requirement 2013-10-30 20:03:30 +00:00
Keith Duncan 20b6a9776c Add a permission change state to SQRLInstaller
This is in aid of preventing time-of-check time-of-use race conditions where we check the code sign signature of the update bundle, and a new bundle is inserted after the check but before the install.
2013-10-30 18:56:53 +00:00
48 changed files with 2172 additions and 2301 deletions
+57 -23
View File
@@ -13,10 +13,10 @@ 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) below.
on server side support for determining whether an update is required, see
[Server Support](#server-support).
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)
@@ -26,13 +26,16 @@ updates installed are valid.
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Run `script/bootstrap` from within the submodule
1. Add a reference to Squirrel.xcodeproj to your project
1. Add references to Squirrel.xcodeproj and its [dependencies](#dependencies) 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)
@@ -60,7 +63,16 @@ Once Squirrel is added to your project, you need to configure and start it.
#import <Squirrel/Squirrel.h>
- (void)applicationDidFinishLaunching:(NSNotification *)notification {
self.updater = [[SQRLUpdater alloc] initWithUpdateRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://mycompany.com/myapp/latest"]]];
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]];
// Check for updates every 4 hours.
[self.updater startAutomaticChecksWithInterval:60 * 60 * 4];
@@ -71,7 +83,26 @@ Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
## Update Notifications
## 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
To know when an update is ready to be installed, you can subscribe to the
`updates` signal on `SQRLUpdater`:
@@ -95,10 +126,26 @@ If you want to install a downloaded update and automatically relaunch afterward,
}];
```
# Update JSON Format
# Server Support
Squirrel requests the URL you provide with `Accept: application/json` and
expects the following schema in response:
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:
```json
{
@@ -116,20 +163,7 @@ 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
# 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.
"pub_date" if present must be formatted according to ISO 8601.
# User Interface
File diff suppressed because it is too large Load Diff
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
LastUpgradeVersion = "0510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -15,22 +15,8 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
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"
BuildableName = "shipit-installer"
BlueprintName = "ShipIt Installer"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</BuildActionEntry>
@@ -42,6 +28,16 @@
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.xctest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
LastUpgradeVersion = "0510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -29,7 +29,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.octest"
BuildableName = "SquirrelTests.xctest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
@@ -47,7 +47,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.octest"
BuildableName = "SquirrelTests.xctest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
LastUpgradeVersion = "0510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
+22
View File
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Squirrel.xcodeproj">
</FileRef>
<FileRef
location = "group:External/Mantle/Mantle.xcodeproj">
</FileRef>
<FileRef
location = "group:External/ReactiveCocoa/ReactiveCocoaFramework/ReactiveCocoa.xcodeproj">
</FileRef>
<FileRef
location = "group:External/ReactiveCocoa/external/specta/Specta.xcodeproj">
</FileRef>
<FileRef
location = "group:External/ReactiveCocoa/external/expecta/Expecta.xcodeproj">
</FileRef>
<FileRef
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
</FileRef>
</Workspace>
+16 -13
View File
@@ -97,12 +97,15 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
[subscriber.disposable addDisposable:SQRLCreateTransaction(name, description)];
[self subscribe:subscriber];
}]
setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
return [RACDisposable disposableWithBlock:^{
[subscriptionDisposable dispose];
[transactionDisposable dispose];
}];
}] setNameWithFormat:@"[%@] -sqrl_addTransactionWithName: %@ description: %@", self.name, name, description];
}
- (RACSignal *)sqrl_addSubscriptionTransactionWithName:(NSString *)name description:(NSString *)descriptionFormat, ... {
@@ -114,13 +117,13 @@ static RACDisposable *SQRLCreateTransaction(NSString *name, NSString *descriptio
va_end(args);
}
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
[self subscribe:subscriber];
[transactionDisposable dispose];
}]
setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *transactionDisposable = SQRLCreateTransaction(name, description);
RACDisposable *subscriptionDisposable = [self subscribe:subscriber];
[transactionDisposable dispose];
return subscriptionDisposable;
}] setNameWithFormat:@"[%@] -sqrl_addSubscriptionTransactionWithName: %@ description: %@", self.name, name, description];
}
@end
+20 -8
View File
@@ -21,32 +21,44 @@ extern const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode;
@class RACSignal;
// Implements the verification of Apple code signatures.
// Implements the verification of Apple code signatures and requirements.
@interface SQRLCodeSignature : MTLModel
// A serialized version of the `SecRequirementRef` that the receiver was
// initialized with.
@property (nonatomic, copy, readonly) NSData *requirementData;
// Determines the code signature of the currently-executing application.
// 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.
//
// error - If not NULL, set to any error that occurs.
//
// Returns a `SQRLCodeSignature`, or nil if an error occurs retrieving the
// signature for the running code.
// designated requirement for the running code.
+ (instancetype)currentApplicationSignature:(NSError **)error;
// Initializes the receiver with the given requirement.
// 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`.
//
// requirement - The code requirement for tested bundles. This must not be NULL.
- (id)initWithRequirement:(SecRequirementRef)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;
// Verifies that the code signature of the specified bundle matches the receiver.
// Verifies the code signature of the specified bundle and verifies that the
// bundle meets the receiver's requirement.
//
// 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 code signature was not verified successfully.
// if the requirement was not verified successfully.
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL;
@end
+54 -38
View File
@@ -19,9 +19,14 @@ 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 property is automatically retained.
@property (atomic) id 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;
@end
@@ -37,8 +42,49 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
#pragma mark Lifecycle
+ (instancetype)currentApplicationSignature:(NSError **)error {
return [self modelWithDictionary:nil error:error];
+ (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];
}
- (id)initWithRequirement:(SecRequirementRef)requirement {
@@ -49,41 +95,12 @@ 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 create:^(id<RACSubscriber> subscriber) {
return [[RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
SecStaticCodeRef staticCode = NULL;
OSStatus result = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
@@ -100,10 +117,8 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (failureReason != nil) userInfo[NSLocalizedFailureReasonErrorKey] = failureReason;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorCouldNotCreateStaticCode userInfo:userInfo]];
return;
return nil;
}
if (subscriber.disposable.disposed) return;
CFErrorRef validityError = NULL;
result = SecStaticCodeCheckValidityWithErrors(staticCode, kSecCSCheckAllArchitectures, (__bridge SecRequirementRef)self.requirement, &validityError);
@@ -121,10 +136,11 @@ const NSInteger SQRLCodeSignatureErrorCouldNotCreateStaticCode = -2;
if (validityError != NULL) userInfo[NSUnderlyingErrorKey] = (__bridge NSError *)validityError;
[subscriber sendError:[NSError errorWithDomain:SQRLCodeSignatureErrorDomain code:SQRLCodeSignatureErrorDidNotPass userInfo:userInfo]];
return;
return nil;
}
[subscriber sendCompleted];
return nil;
}] setNameWithFormat:@"-verifyCodeSignatureOfBundle: %@", bundleURL];
}
+3
View File
@@ -13,6 +13,9 @@
// 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;
+1 -8
View File
@@ -9,13 +9,6 @@
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLDirectoryManager ()
// The application identifier to use in file locations.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
@end
@implementation SQRLDirectoryManager
#pragma mark Lifecycle
@@ -30,7 +23,7 @@
// Should only fallback to when running under otest, where
// NSBundle.mainBundle doesn't return useful data.
if (identifier == nil) {
identifier = NSRunningApplication.currentApplication.localizedName;
identifier = NSProcessInfo.processInfo.processName;
}
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
+17
View File
@@ -0,0 +1,17 @@
//
// 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;
+23 -19
View File
@@ -11,7 +11,7 @@
// The domain for errors originating within SQRLInstaller.
extern NSString * const SQRLInstallerErrorDomain;
// There was an error copying the target bundle to the backup location.
// There was an error copying the target or update bundle to a backup location.
extern const NSInteger SQRLInstallerErrorBackupFailed;
// There was an error replacing the target bundle with the update.
@@ -34,35 +34,39 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
// There was an error moving a bundle across volumes.
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
@class RACSignal;
@class RACAction;
@class SQRLShipItState;
@class SQRLDirectoryManager;
// There was an error changing the file permissions of the update.
extern const NSInteger SQRLInstallerErrorChangingPermissions;
// Performs the installation of an update, using the `SQRLShipItState` on disk,
// as located by `SQRLDirectoryManager`.
@class RACCommand;
// Performs the installation of an update, saving its intermediate state to user
// defaults.
//
// This class is meant to be used only after the app that will be updated has
// terminated.
@interface SQRLInstaller : NSObject
// Attempts to install the update or resume an in-progress installation.
// Initializes an installer using the given application identifier, which is
// used to scope resumable state stored to user defaults.
//
// Returns a signal which completes or errors on an unspecified scheduler when
// 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
// installation has completed or failed.
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state;
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
// Aborts an installation, and attempts to restore the old version of the
// application if necessary.
// When executed with a `SQRLShipItRequest`, aborts an installation, and
// attempts to restore the old version of the application if necessary.
//
// This must not be executed while `installUpdateWithState:` is executing.
// This must not be executed while `installUpdateCommand` is executing.
//
// Returns a signal while completes or errors on an unspecified scheduler once
// Each execution will complete or error on an unspecified scheduler once
// aborting/recovery has finished.
- (RACSignal *)abortInstallationWithState:(SQRLShipItState *)state;
// Initializes an installer using the given directory manager to read and write the
// state of the installation.
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
@end
+348 -345
View File
@@ -12,12 +12,12 @@
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
#import "SQRLShipItRequest.h"
#import <libkern/OSAtomic.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/xattr.h>
#import "SQRLInstallerOwnedBundle.h"
NSString * const SQRLInstallerErrorDomain = @"SQRLInstallerErrorDomain";
@@ -28,85 +28,58 @@ const NSInteger SQRLInstallerErrorInvalidBundleVersion = -4;
const NSInteger SQRLInstallerErrorMissingInstallationData = -5;
const NSInteger SQRLInstallerErrorInvalidState = -6;
const NSInteger SQRLInstallerErrorMovingAcrossVolumes = -7;
const NSInteger SQRLInstallerErrorChangingPermissions = -8;
// 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;
NSString * const SQRLShipItInstallationAttemptsKey = @"SQRLShipItInstallationAttempts";
NSString * const SQRLInstallerOwnedBundleKey = @"SQRLInstallerOwnedBundle";
@interface SQRLInstaller ()
// Finds the state file to read and write from.
@property (nonatomic, strong, readonly) SQRLDirectoryManager *directoryManager;
// The defaults domain to store all resumable state in.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
// Reads the given key from `state`, failing if it's not set.
// The bundle currently owned by this installer.
//
// 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.
// 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.
//
// Returns a signal which synchronously sends the non-nil read value then
// completes, or errors.
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state;
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request;
// Performs the remaining stages of installation, as specified by `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.
//
// state - The installation state. This must not be nil.
// request - The request whose update should be prepared and validated.
//
// Returns a signal which will complete or error on an unspecified thread.
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state;
// Returns a signal which sends the owned & validated bundle URL then completes,
// or errors.
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request;
// Moves the specified bundle to a backup location.
// Saves a `SQRLInstallerOwnedBundle` for the targetBundleURL to the
// preferences, then moves the targetBundleURL to an owned directory.
//
// bundleURL - The URL to the bundle that should be backed up. This must not be
// nil.
// request - The request whose target should be removed in preparation of an
// update being installed.
//
// 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;
// Returns a signal which completes, or errors.
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request;
// Deletes a bundle that was backed up using -backUpBundleAtURL:.
// Deletes a bundle that was moved into place using -moveAndTakeOwnershipOfBundleAtURL:.
//
// backupURL - The URL to the backup bundle, as sent from -backUpBundleAtURL:.
// bundleURL - The URL to the backup bundle, as sent from -moveAndTakeOwnershipOfBundleAtURL:.
// This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (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;
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL;
// Moves `sourceURL` to `targetURL`.
//
@@ -118,7 +91,7 @@ typedef struct {
// sourceURL - The URL to move from. This must not be nil.
//
// Retruns a signal which will synchronously complete or error.
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL;
// Recursively clears the quarantine extended attribute from the given
// directory.
@@ -132,56 +105,96 @@ typedef struct {
// 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)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager {
NSParameterAssert(directoryManager != nil);
- (id)initWithApplicationIdentifier:(NSString *)applicationIdentifier {
NSParameterAssert(applicationIdentifier != nil);
self = [super init];
if (self == nil) return nil;
_directoryManager = directoryManager;
_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];
}];
return self;
}
#pragma mark Actions
#pragma mark Preferences
- (RACSignal *)installUpdateWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
- (SQRLInstallerOwnedBundle *)ownedBundle {
id archiveData = CFBridgingRelease(CFPreferencesCopyValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost));
if (![archiveData isKindOfClass:NSData.class]) return nil;
return [[self
resumeInstallationFromState:state]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
SQRLInstallerOwnedBundle *ownedBundle = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
if (![ownedBundle isKindOfClass:SQRLInstallerOwnedBundle.class]) return nil;
return ownedBundle;
}
- (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];
- (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);
}
#pragma mark Installer State
#pragma mark Properties
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
- (RACSignal *)getRequiredKey:(NSString *)key fromRequest:(SQRLShipItRequest *)request {
NSParameterAssert(key != nil);
NSParameterAssert(state != nil);
NSParameterAssert(request != nil);
return [[RACSignal
defer:^{
id value = [state valueForKey:key];
id value = [request valueForKey:key];
if (value == nil) {
NSString *errorDescription = [NSString stringWithFormat:NSLocalizedString(@"Missing %@", nil), key];
return [RACSignal error:[self missingDataErrorWithDescription:errorDescription]];
@@ -189,309 +202,210 @@ typedef struct {
return [RACSignal return:value];
}
}]
setNameWithFormat:@"%@ -getRequiredKey: %@ fromState: %@", self, key, state];
setNameWithFormat:@"%@ -getRequiredKey: %@ fromRequest: %@", self, key, request];
}
- (RACSignal *)resumeInstallationFromState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
#pragma mark Installer States
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:) },
};
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
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];
return [[[[[[[self
ownedTemporaryDirectoryURL]
flattenMap:^(NSURL *directoryURL) {
return [self copyBundleAtURL:request.updateBundleURL toDirectory:directoryURL];
}]
setNameWithFormat:@"%@ -clearQuarantineWithState: %@", self, state];
flattenMap:^(NSURL *bundleURL) {
return [[[self
clearQuarantineForDirectory:bundleURL]
ignoreValues]
concat:[RACSignal return: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];
}
- (RACSignal *)backUpWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
return [[[[RACSignal
zip:@[
[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];
}
[self ownedTemporaryDirectoryURL],
[self codeSignatureForBundleAtURL:request.targetBundleURL],
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
NSURL *targetBundleURL = request.targetBundleURL;
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
return [skipBackup flattenMap:^(NSNumber *skip) {
if (skip.boolValue) {
return [RACSignal empty];
} else {
return [self backUpBundleAtURL:bundleURL];
}
}];
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
}]
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];
doNext:^(SQRLInstallerOwnedBundle *ownedBundle) {
self.ownedBundle = ownedBundle;
}]
setNameWithFormat:@"%@ -backUpWithState: %@", self, state];
flattenMap:^(SQRLInstallerOwnedBundle *ownedBundle) {
return [self installItemToURL:ownedBundle.temporaryURL fromURL:ownedBundle.originalURL];
}]
setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request];
}
- (RACSignal *)installWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
- (RACSignal *)installRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
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];
}
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]];
}]
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]];
doCompleted:^{
self.ownedBundle = nil;
}];
}]
flatten]
setNameWithFormat:@"%@ -installWithState: %@", self, state];
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];
}
- (RACSignal *)verifyInPlaceWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
- (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
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]]];
}]
flatten]
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", self, state];
}
#pragma mark Backing Up
- (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];
}]
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];
}
NSString *tmpPath = [NSTemporaryDirectory() stringByResolvingSymlinksInPath];
NSString *template = [NSString stringWithFormat:@"%@.XXXXXXXX", self.applicationIdentifier];
// Also remove the temporary directory that the backup lived in.
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
char *fullTemplate = strdup([tmpPath stringByAppendingPathComponent:template].UTF8String);
@onExit {
free(fullTemplate);
};
// However, use rmdir() to skip it in case there are other files
// contained within (for whatever reason).
if (rmdir(temporaryDirectoryURL.path.fileSystemRepresentation) != 0) {
if (mkdtemp(fullTemplate) == NULL) {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
return [RACSignal empty];
NSURL *URL = [NSURL fileURLWithPath:[NSFileManager.defaultManager stringWithFileSystemRepresentation:fullTemplate length:strlen(fullTemplate)] isDirectory:YES];
return [RACSignal return:URL];
}]
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
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 *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL {
- (RACSignal *)copyBundleAtURL:(NSURL *)bundleURL toDirectory:(NSURL *)directoryURL {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(signature != nil);
NSParameterAssert(directoryURL != nil);
return [[[signature
verifyBundleAtURL:bundleURL]
catch:^(NSError *error) {
if (backupBundleURL == nil) return [RACSignal error:error];
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:bundleURL.lastPathComponent];
return [[[RACSignal
defer:^{
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
return [[[RACSignal
defer:^{
NSError *error;
BOOL copy = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:newBundleURL error:&error];
if (!copy) return [RACSignal error:error];
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);
}];
return [RACSignal return:newBundleURL];
}]
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@ recoveringUsingBackupAtURL: %@", self, bundleURL, signature, backupBundleURL];
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];
}
#pragma mark Installation
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
- (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];
return [[[RACSignal
defer:^{
NSError *error;
if ([NSFileManager.defaultManager removeItemAtURL:bundleURL error:&error]) {
return [RACSignal empty];
} else {
return [RACSignal error:error];
}
}]
setNameWithFormat:@"%@ -checkWhetherBundlePreviouslyAtURL: %@ wasInstalledAtURL: %@", self, sourceURL, targetURL];
then:^{
// Also remove the temporary directory that the backup lived in.
NSURL *temporaryDirectoryURL = bundleURL.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 {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
}]
setNameWithFormat:@"%@ -deleteOwnedBundleAtURL: %@", self, bundleURL];
}
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
#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 {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(signature != nil);
return [[[self
takeOwnershipOfDirectory:bundleURL]
then:^{
return [signature verifyBundleAtURL:bundleURL];
}]
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@", self, bundleURL, signature];
}
#pragma mark Installation
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
@@ -544,7 +458,7 @@ typedef struct {
return YES;
}];
return enumerator.allObjects.rac_signal;
return enumerator.rac_sequence.signal;
}]
flattenMap:^(NSURL *URL) {
const char *path = URL.path.fileSystemRepresentation;
@@ -568,6 +482,95 @@ typedef struct {
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 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
@@ -0,0 +1,37 @@
//
// 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
@@ -0,0 +1,23 @@
//
// SQRLInstallerOwnedBundle.m
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLInstallerOwnedBundle.h"
#import <ReactiveCocoa/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
+81
View File
@@ -0,0 +1,81 @@
//
// SQRLShipItConnection.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
@class SQRLShipItRequest;
// The domain for errors originating within SQRLShipItConnection.
extern NSString * const SQRLShipItConnectionErrorDomain;
// The ShipIt service could not be started.
extern const NSInteger SQRLShipItConnectionErrorCouldNotStartService;
// Installing an update requires the coordination of multiple processes,
// `SQRLShipItConnection` is responsible for submitting the launchd jobs
// required to perform an installation request.
//
// The multiprocess approach to waiting for termination and installing is
// designed to keep uses of AppKit API (i.e. `NSRunningApplication`) out of the
// installer process - which may be running in the root bootstrap context - and
// put them in a process running in the user bootstrap context. These processes
// can then communicate using the file system as a message bus.
//
// Frustratingly although LaunchServices is daemon and root safe since 10.5, the
// LaunchServices API for querying the running applications isn't public
// (internally `NSRunningApplication` uses this LaunchServices private API,
// though of course this is subject to change and `NSRunningApplication` could
// also do other non-root safe things).
//
// When a user application running Squirrel wants to install an update, Squirrel
// submits two launchd jobs, one which will wait for application termination and
// write an empty file to the given location when the criteria are met, and the
// other which will wait for that file to appear and then perform the install as
// before. The "wait for termination" job is always submitted to the user
// domain, but the installer job is submitted to a domain based on whether the
// install location is writable by the current user.
//
// To perform the relaunch when the install is complete, it is safe to use the
// LaunchServices API from the installer process, the app will be launched in
// the user's GUI session.
//
// From <https://developer.apple.com/library/mac/technotes/tn2083/>
//
// > If the EUID of the calling process is zero, the application is launched in
// > the context of the currently active GUI login session. If there is no
// > currently active GUI login session (no one is logged in, or a logged in
// > user has fast user switched to the login window), the behavior is
// > unspecified (r. 5321293).
@interface SQRLShipItConnection : NSObject
// Returns the label for the ShipIt launchd job.
+ (NSString *)shipItInstallerJobLabel;
// Designated initialiser.
//
// privileged - Determines which launchd domain to launch the job in.
// If YES, ShipIt is launched in the root domain, otherwise it is
// launched in the current user’s domain.
//
// Returns an initialised connection which can be used to start an install.
- (instancetype)initWithRootPrivileges:(BOOL)rootPrivileges;
// Submits the request to the process network which will wait for termination
// and then install.
//
// After sending the request, the update will be attempted when the sending
// process terminates.
//
// request - The install parameters, target bundle, update bundle, whether to
// launch when install is complete etc. Must not be nil.
//
// Returns a signal which will complete, or error, on a background scheduler.
- (RACSignal *)sendRequest:(SQRLShipItRequest *)request;
@end
+203
View File
@@ -0,0 +1,203 @@
//
// SQRLShipItConnection.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLShipItConnection.h"
#import "EXTScope.h"
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
#import "SQRLShipItRequest.h"
NSString * const SQRLShipItConnectionErrorDomain = @"SQRLShipItConnectionErrorDomain";
const NSInteger SQRLShipItConnectionErrorCouldNotStartService = 1;
@interface SQRLShipItConnection ()
@property (readonly, nonatomic, assign) BOOL privileged;
@end
@implementation SQRLShipItConnection
+ (NSString *)shipItInstallerJobLabel {
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
}
+ (NSMutableDictionary *)jobDictionaryWithLabel:(NSString *)jobLabel executableName:(NSString *)executableName arguments:(NSArray *)arguments {
NSParameterAssert(jobLabel != nil);
NSParameterAssert(executableName != nil);
NSParameterAssert(arguments != nil);
NSMutableDictionary *jobDict = [NSMutableDictionary dictionary];
jobDict[@(LAUNCH_JOBKEY_LABEL)] = jobLabel;
jobDict[@(LAUNCH_JOBKEY_NICE)] = @(-1);
jobDict[@(LAUNCH_JOBKEY_ENABLETRANSACTIONS)] = @NO;
jobDict[@(LAUNCH_JOBKEY_THROTTLEINTERVAL)] = @2;
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
NSMutableArray *fullArguments = [NSMutableArray arrayWithObject:[squirrelBundle pathForResource:executableName ofType:nil]];
[fullArguments addObjectsFromArray:arguments];
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = fullArguments;
return jobDict;
}
+ (RACSignal *)shipItInstallerJobDictionary {
NSString *jobLabel = self.shipItInstallerJobLabel;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
return [directoryManager applicationSupportURL];
}]
map:^(NSURL *appSupportURL) {
NSMutableArray *arguments = [[NSMutableArray alloc] init];
// Pass in the service name so ShipIt knows how to broadcast itself.
[arguments addObject:jobLabel];
NSMutableDictionary *jobDict = [self jobDictionaryWithLabel:jobLabel executableName:@"shipit-installer" arguments:arguments];
jobDict[@(LAUNCH_JOBKEY_KEEPALIVE)] = @{
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
};
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stdout.log"].path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = [appSupportURL URLByAppendingPathComponent:@"ShipIt_stderr.log"].path;
return jobDict;
}]
setNameWithFormat:@"+shipItInstallerJobDictionary"];
}
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
},
};
AuthorizationRights rights = {
.count = sizeof(rightItems) / sizeof(*rightItems),
.items = rightItems,
};
NSString *prompt = NSLocalizedString(@"An update is ready to install.", @"SQRLShipItConnection, launch shipit, authorization prompt");
NSString *iconName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
NSString *iconPath = (iconName == nil ? nil : [NSBundle.mainBundle pathForImageResource:iconName]);
AuthorizationItem environmentItems[] = {
{
.name = kAuthorizationEnvironmentPrompt,
.valueLength = strlen(prompt.UTF8String),
.value = (void *)prompt.UTF8String,
},
{
.name = kAuthorizationEnvironmentIcon,
.valueLength = (iconPath == nil ? 0 : strlen(iconPath.UTF8String)),
.value = (void *)iconPath.UTF8String,
},
};
AuthorizationEnvironment environment = {
.count = sizeof(environmentItems) / sizeof(*environmentItems),
.items = environmentItems,
};
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return nil;
}]
setNameWithFormat:@"+shipItAuthorization"];
}
- (instancetype)initWithRootPrivileges:(BOOL)rootPrivileges {
self = [self init];
if (self == nil) return nil;
_privileged = rootPrivileges;
return self;
}
- (RACSignal *)sendRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
return [[[self
submitInstallerJobForRequestIfNeeded:request]
concat:[RACSignal defer:^{
}]]
setNameWithFormat:@"%@ -sendRequest: %@", self, request];
}
- (RACSignal *)submitInstallerJobForRequestIfNeeded:(SQRLShipItRequest *)request {
// TODO implement lazy submission when the job is already loaded in launchd
CFStringRef domain = NULL; RACSignal *authorization;
if (self.privileged) {
domain = kSMDomainSystemLaunchd;
authorization = self.class.shipItAuthorization;
} else {
domain = kSMDomainUserLaunchd;
authorization = [RACSignal return:nil];
}
return [[[RACSignal
zip:@[
self.class.shipItInstallerJobDictionary,
authorization,
] reduce:^(NSDictionary *job, SQRLAuthorization *authorization) {
return [self submitJob:job domain:(__bridge id)domain authorization:authorization];
}]
flatten]
setNameWithFormat:@"%@ -submitInstallerJobForRequestIfNeeded: %@", self, request];
}
- (RACSignal *)submitJob:(NSDictionary *)job domain:(NSString *)domain authorization:(SQRLAuthorization *)authorizationValue {
return [[RACSignal
defer:^{
NSString *jobLabel = job[@(LAUNCH_JOBKEY_LABEL)];
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove((__bridge CFStringRef)domain, (__bridge CFStringRef)jobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job %@: %@", jobLabel, error);
}
}
if (!SMJobSubmit((__bridge CFStringRef)domain, (__bridge CFDictionaryRef)job, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
return [RACSignal empty];
}]
setNameWithFormat:@"%@ -submitJob: %@ domain: %@ authorization: %@", self, job, domain, authorizationValue];
}
@end
-34
View File
@@ -1,34 +0,0 @@
//
// SQRLShipItLauncher.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
// The domain for errors originating within SQRLShipItLauncher.
extern NSString * const SQRLShipItLauncherErrorDomain;
// The ShipIt service could not be started.
extern const NSInteger SQRLShipItLauncherErrorCouldNotStartService;
// Responsible for launching the ShipIt service to actually install an update.
@interface SQRLShipItLauncher : NSObject
// Returns the label for the ShipIt launchd job.
+ (NSString *)shipItJobLabel;
// Attempts to launch ShipIt.
//
// privileged - Determines which launchd domain to launch the job in.
// If YES, ShipIt is launched in the root domain, otherwise it is
// launched in the current user’s domain.
//
// Returns a signal which will complete, or error, on a background scheduler.
+ (RACSignal *)launchPrivileged:(BOOL)privileged;
@end
-153
View File
@@ -1,153 +0,0 @@
//
// SQRLShipItLauncher.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLShipItLauncher.h"
#import "EXTScope.h"
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
@implementation SQRLShipItLauncher
+ (NSString *)shipItJobLabel {
NSString *currentAppIdentifier = NSBundle.mainBundle.bundleIdentifier ?: [NSString stringWithFormat:@"%@:%d", NSProcessInfo.processInfo.processName, NSProcessInfo.processInfo.processIdentifier];
return [currentAppIdentifier stringByAppendingString:@".ShipIt"];
}
+ (RACSignal *)shipItJobDictionary {
NSString *jobLabel = self.shipItJobLabel;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
return [directoryManager applicationSupportURL];
}]
map:^(NSURL *appSupportURL) {
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
NSMutableDictionary *jobDict = [NSMutableDictionary dictionary];
jobDict[@(LAUNCH_JOBKEY_LABEL)] = jobLabel;
jobDict[@(LAUNCH_JOBKEY_NICE)] = @(-1);
jobDict[@(LAUNCH_JOBKEY_ENABLETRANSACTIONS)] = @NO;
jobDict[@(LAUNCH_JOBKEY_THROTTLEINTERVAL)] = @2;
jobDict[@(LAUNCH_JOBKEY_KEEPALIVE)] = @{
@(LAUNCH_JOBKEY_KEEPALIVE_SUCCESSFULEXIT): @NO
};
jobDict[@(LAUNCH_JOBKEY_MACHSERVICES)] = @{
jobLabel: @YES
};
NSMutableArray *arguments = [[NSMutableArray alloc] init];
[arguments addObject:[squirrelBundle URLForResource:@"ShipIt" withExtension:nil].path];
// Pass in the service name so ShipIt knows how to broadcast itself.
[arguments addObject:jobLabel];
jobDict[@(LAUNCH_JOBKEY_PROGRAMARGUMENTS)] = arguments;
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"];
}
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
create:^(id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
},
};
AuthorizationRights rights = {
.count = sizeof(rightItems) / sizeof(*rightItems),
.items = rightItems,
};
NSString *prompt = NSLocalizedString(@"An update is ready to install.", @"SQRLShipItLauncher, launch shipit, authorization prompt");
NSString *iconName = [NSBundle.mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
NSString *iconPath = (iconName == nil ? nil : [NSBundle.mainBundle.resourceURL URLByAppendingPathComponent:iconName].path);
AuthorizationItem environmentItems[] = {
{
.name = kAuthorizationEnvironmentPrompt,
.valueLength = strlen(prompt.UTF8String),
.value = (void *)prompt.UTF8String,
},
{
.name = kAuthorizationEnvironmentIcon,
.valueLength = iconPath == nil ? 0 : strlen(iconPath.UTF8String),
.value = (void *)iconPath.UTF8String,
},
};
AuthorizationEnvironment environment = {
.count = sizeof(environmentItems) / sizeof(*environmentItems),
.items = environmentItems,
};
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
}]
setNameWithFormat:@"+shipItAuthorization"];
}
+ (RACSignal *)launchPrivileged:(BOOL)privileged {
return [[[RACSignal
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
AuthorizationRef authorization = authorizationValue.authorization;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job: %@", error);
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
return [RACSignal empty];
}]
flatten]
setNameWithFormat:@"+launchPrivileged: %i", (int)privileged];
}
@end
+106
View File
@@ -0,0 +1,106 @@
//
// 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 URL.
//
// URL - The file location to read from. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
// complete, or error.
+ (RACSignal *)readFromURL:(NSURL *)URL;
// Reads a `SQRLShipItState` from encoded data.
//
// data - Serialised request from `serialization`.
//
// Returns a signal which decodes the serialisation and sends a
// `SQRLShipItRequest` then completes, or errors.
+ (RACSignal *)readFromData:(NSData *)data;
// 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;
// Writes the receiver's serialization to disk, at the location specified by the
// URL.
//
// URL - The file location to write to. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeToURL:(NSURL *)URL;
// Encode the receiver for saving to disk or sending over IPC.
//
// Returns a signal which sends a `NSData` then completes, or errors.
- (RACSignal *)serialization;
@end
+189
View File
@@ -0,0 +1,189 @@
//
// SQRLShipItRequest.m
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/ReactiveCocoa.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 *)readFromURL:(NSURL *)URL {
NSParameterAssert(URL != nil);
return [[[[RACSignal
defer:^{
NSError *error;
NSData *data = [self readFromURL:URL 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 *)writeToURL:(NSURL *)URL {
NSParameterAssert(URL != nil);
return [[[[self
serialization]
flattenMap:^(NSData *data) {
NSError *error;
BOOL write = [self writeData:data toURL:URL error:&error];
if (!write) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
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
@@ -1,117 +0,0 @@
//
// 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
@@ -1,122 +0,0 @@
//
// 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
-32
View File
@@ -1,32 +0,0 @@
//
// SQRLTerminationListener.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-04.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
// Waits for the termination of a GUI application.
@interface SQRLTerminationListener : NSObject
// Initializes the receiver to wait for termination of the app at the given
// location.
//
// bundleURL - The URL to the application bundle to watch. This must not be nil.
// bundleID - The identifier of the application bundle to watch. This must not
// be nil.
- (id)initWithURL:(NSURL *)bundleURL bundleIdentifier:(NSString *)bundleID;
// Lazily waits for termination of all instances of the application identified
// at initialization.
//
// Returns a signal which send an `NSRunningApplication` for each instance of the
// application that is being watched (before the instance terminates), then
// completes on a background scheduler.
- (RACSignal *)waitForTermination;
@end
-89
View File
@@ -1,89 +0,0 @@
//
// SQRLTerminationListener.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-04.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTerminationListener.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLTerminationListener ()
@property (nonatomic, copy, readonly) NSURL *bundleURL;
@property (nonatomic, copy, readonly) NSString *bundleIdentifier;
// Waits for the process identified by the given PID to terminate.
//
// Returns a signal which sends `processIdentifier` as soon as the process is
// being monitored, then completes once it exits, all on a background thread.
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier;
@end
@implementation SQRLTerminationListener
#pragma mark Lifecycle
- (id)initWithURL:(NSURL *)bundleURL bundleIdentifier:(NSString *)bundleID {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(bundleID != nil);
self = [super init];
if (self == nil) return nil;
_bundleURL = bundleURL.URLByStandardizingPath;
_bundleIdentifier = [bundleID copy];
return self;
}
#pragma mark Termination Listening
- (RACSignal *)waitForTermination {
return [[[[RACSignal
defer:^{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:self.bundleIdentifier];
return apps.rac_signal;
}]
filter:^(NSRunningApplication *application) {
return [application.bundleURL.URLByStandardizingPath isEqual:self.bundleURL];
}]
flattenMap:^(NSRunningApplication *application) {
return [[self
waitForTerminationOfProcessIdentifier:application.processIdentifier]
mapReplace:application];
}]
setNameWithFormat:@"%@ -waitForTermination", self];
}
- (RACSignal *)waitForTerminationOfProcessIdentifier:(pid_t)processIdentifier {
return [[RACSignal
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, ^{
[subscriber sendNext:@(processIdentifier)];
});
dispatch_source_set_event_handler(source, ^{
[subscriber sendCompleted];
});
dispatch_resume(source);
[subscriber.disposable addDisposable:[RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
dispatch_release(source);
}]];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
}
#pragma mark NSObject
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p>{ bundleURL: %@, bundleIdentifier: %@ }", self.class, self, self.bundleURL, self.bundleIdentifier];
}
@end
+28 -7
View File
@@ -6,6 +6,20 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
// 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;
@@ -43,7 +57,7 @@ extern NSString * const SQRLUpdaterServerDataErrorKey;
// error with code `SQRLUpdaterErrorInvalidJSON` is generated.
extern NSString * const SQRLUpdaterJSONObjectErrorKey;
@class RACAction;
@class RACCommand;
@class RACDisposable;
@class RACSignal;
@@ -52,12 +66,19 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Kicks off a check for updates.
//
// If an update is available, it will be sent on
// `[checkForUpdatesAction deferred]` and `updates` once downloaded.
@property (nonatomic, strong, readonly) RACAction *checkForUpdatesAction;
// If an update is available, it will be sent on `updates` once downloaded.
@property (nonatomic, strong, readonly) RACCommand *checkForUpdatesCommand;
// The current state of the manager.
//
// This property is KVO-compliant.
@property (atomic, readonly) SQRLUpdaterState state;
// Sends an `SQRLDownloadedUpdate` object on the main thread whenever a new
// update is available. Completes when the receiver deallocates.
// update is available.
//
// This signal is actually just `checkForUpdatesCommand.executionSignals`,
// flattened for convenience.
@property (nonatomic, strong, readonly) RACSignal *updates;
// The request that will be sent to check for updates.
@@ -104,8 +125,8 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
// other exit mechanism.
//
// After subscribing to this signal, the receiver is responsible for terminating
// the application upon success. The app must not be terminated in any other way
// After invoking this method, the receiver is responsible for terminating the
// application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
+144 -190
View File
@@ -14,10 +14,11 @@
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLShipItConnection.h"
#import "SQRLShipItRequest.h"
#import "SQRLUpdate.h"
#import "SQRLZipArchiver.h"
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@@ -38,14 +39,14 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@interface SQRLUpdater ()
@property (atomic, readwrite) SQRLUpdaterState state;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
// Lazily launches ShipIt upon first subscription.
//
// Sends completed or error.
@property (nonatomic, strong, readonly) RACSignal *shipItLauncher;
// When executed with an `SQRLShipItState`, launches ShipIt.
@property (nonatomic, strong, readonly) RACCommand *shipItLauncher;
// Lazily removes outdated temporary directories (used for previous updates)
// upon first subscription.
@@ -124,27 +125,16 @@ 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 {
RACSubject *_updates;
}
@implementation SQRLUpdater
#pragma mark Properties
- (RACSignal *)updates {
return [_updates
deliverOn:RACScheduler.mainThreadScheduler];
return [[self.checkForUpdatesCommand.executionSignals
concat]
setNameWithFormat:@"%@ -updates", self];
}
#pragma mark Lifecycle
@@ -175,11 +165,12 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
#endif
}
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_prunedUpdateDirectories = [[[[[[[[RACSignal
_prunedUpdateDirectories = [[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
@@ -189,7 +180,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return YES;
}];
return [[enumerator.allObjects.rac_signal
return [[enumerator.rac_sequence.signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
@@ -201,127 +192,106 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
}
}];
}]
collect]
// These operators ensure that the actual work only executes once,
// without starting it immediately.
concat:[RACSignal never]]
shareWhileActive]
take:1]
flattenMap:^(NSArray *URLs) {
return URLs.rac_signal;
}]
replayLazily]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
_checkForUpdatesAction = [[[RACSignal
defer:^{
@strongify(self);
return [RACSignal return:self.updateRequest];
}]
flattenMap:^(NSURLRequest *request) {
return [self checkForUpdates:request];
}]
action];
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
_updates = [[RACSubject
subject]
setNameWithFormat:@"%@ updates", self];
// TODO: Maybe allow this to be an argument to the command?
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
_shipItLauncher = [[[[[[[RACSignal
defer:^{
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
// 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;
NSNumber *targetWritable = nil;
NSError *targetWritableError = nil;
BOOL gotWritable = [targetURL getResourceValue:&targetWritable forKey:NSURLIsWritableKey error:&targetWritableError];
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 we can't determine whether it can be written, assume nonprivileged and
// wait for another, more canonical error.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
}]
// These operators ensure that the actual work only executes once,
// without starting it immediately. Super hacky.
concat:[RACSignal return:RACUnit.defaultUnit]]
concat:[RACSignal never]]
shareWhileActive]
take:1]
ignoreValues]
setNameWithFormat:@"shipItLauncher"];
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 = [[RACCommand alloc] initWithSignalBlock:^(SQRLShipItRequest *request) {
NSURL *targetURL = request.targetBundleURL;
NSNumber *targetWritable = nil;
NSError *targetWritableError = nil;
BOOL gotWritable = [targetURL getResourceValue:&targetWritable forKey:NSURLIsWritableKey error:&targetWritableError];
// If we can't determine whether it can be written, assume
// nonprivileged and wait for another, more canonical error.
SQRLShipItConnection *connection = [[SQRLShipItConnection alloc] initWithRootPrivileges:(gotWritable && !targetWritable.boolValue)];
return [connection sendRequest:request];
}];
return self;
}
- (void)dealloc {
[_updates sendCompleted];
}
#pragma mark Checking for Updates
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval {
@weakify(self);
return [[[[RACSignal
return [[[[[RACSignal
interval:interval onScheduler:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]]
flattenMap:^(id _) {
@strongify(self);
return [[[self.checkForUpdatesAction
signalWithValue:nil]
ignoreValues]
return [[self.checkForUpdatesCommand
execute:RACUnit.defaultUnit]
catch:^(NSError *error) {
NSLog(@"Error checking for updates: %@", error);
return [RACSignal empty];
}];
}]
takeUntil:self.rac_willDeallocSignal]
subscribeCompleted:^{}];
}
- (RACSignal *)checkForUpdates:(NSURLRequest *)request {
NSParameterAssert(request != nil);
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
if (updatesDisabled) return [RACSignal empty];
NSMutableURLRequest *newRequest = [request mutableCopy];
[newRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[[[self.prunedUpdateDirectories
catch:^(NSError *error) {
NSLog(@"Error pruning old updates: %@", error);
return [RACSignal empty];
}]
ignoreValues]
concat:[NSURLConnection rac_sendAsynchronousRequest:newRequest]]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
}]
doNext:^(SQRLDownloadedUpdate *update) {
[self->_updates sendNext:update];
}];
publish]
connect];
}
- (RACSignal *)updateFromJSONData:(NSData *)data {
@@ -437,7 +407,9 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
}
}];
}]
concat:[self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory]]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
}]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
@@ -446,7 +418,7 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
- (RACSignal *)uniqueTemporaryDirectoryForUpdate {
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return [directoryManager applicationSupportURL];
}]
flattenMap:^(NSURL *appSupportURL) {
@@ -483,27 +455,25 @@ 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;
}
NSURL *updateBundleURL = [[enumerator.allObjects.rac_signal
filter:^(NSURL *URL) {
NSString *type = nil;
NSError *error = nil;
if (![URL getResourceValue:&type forKey:NSURLTypeIdentifierKey error:&error]) {
NSLog(@"Error retrieving UTI for item at %@: %@", URL, error);
return NO;
}
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
if (!UTTypeConformsTo((__bridge CFStringRef)type, kUTTypeApplicationBundle)) return NO;
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
NSBundle *bundle = [NSBundle bundleWithURL:URL];
if (bundle == nil) {
NSLog(@"Could not open application bundle at %@", URL);
return NO;
}
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}]
first];
return [bundle.bundleIdentifier isEqual:NSRunningApplication.currentApplication.bundleIdentifier];
}];
if (updateBundleURL != nil) {
return [RACSignal return:updateBundleURL];
@@ -522,12 +492,8 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
}
- (RACSignal *)shipItStateURL {
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return directoryManager.shipItStateURL;
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
return directoryManager.shipItStateURL;
}
#pragma mark Installing Updates
@@ -538,63 +504,50 @@ static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
return [[[[self.signature
verifyBundleAtURL:updateBundle.bundleURL]
concat:[RACSignal return:[[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle]]]
then:^{
SQRLDownloadedUpdate *downloadedUpdate = [[SQRLDownloadedUpdate alloc] initWithUpdate:update bundle:updateBundle];
return [RACSignal return:downloadedUpdate];
}]
flattenMap:^(SQRLDownloadedUpdate *downloadedUpdate) {
return [[self
prepareUpdateForInstallation:downloadedUpdate]
concat:[RACSignal return:downloadedUpdate]];
return [[self prepareUpdateForInstallation:downloadedUpdate] then:^{
return [RACSignal return:downloadedUpdate];
}];
}]
setNameWithFormat:@"%@ -verifyAndPrepareUpdate: %@ fromBundle: %@", self, update, updateBundle];
}
- (RACSignal *)validateExistingState:(SQRLShipItState *)existingState {
return [[RACSignal
defer:^{
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
// If this happens, shit is crazy, because it implies that an
// update is being installed over us right now.
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
};
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal return:existingState];
}]
setNameWithFormat:@"%@ -validateExistingState: %@", self, existingState];
}
- (RACSignal *)prepareUpdateForInstallation:(SQRLDownloadedUpdate *)update {
NSParameterAssert(update != nil);
return [[[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
return [[[[RACSignal
defer:^{
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO];
return [[self.shipItStateURL
flattenMap:^(NSURL *location) {
return [request writeToURL:location];
}]
concat:[RACSignal return:request]];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [self.shipItLauncher execute:request];
}]
ignoreValues]
concat:[RACSignal defer:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
return [state writeUsingURL:self.shipItStateURL];
}]]
concat:self.shipItLauncher]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing update", nil) description:NSLocalizedString(@"An update for %@ is being prepared. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier]
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[SQRLShipItState
readUsingURL:self.shipItStateURL]
flattenMap:^(SQRLShipItState *existingState) {
return [self validateExistingState:existingState];
}]
flattenMap:^(SQRLShipItState *state) {
state.relaunchAfterInstallation = YES;
return [[state
writeUsingURL:self.shipItStateURL]
return [[[[[[self.shipItStateURL
flattenMap:^(NSURL *location) {
return [[[[SQRLShipItRequest
readFromURL:location]
map:^(SQRLShipItRequest *request) {
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [request writeToURL:location];
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
}]
deliverOn:RACScheduler.mainThreadScheduler]
@@ -604,6 +557,7 @@ 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]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
+14 -12
View File
@@ -130,23 +130,25 @@ const NSInteger SQRLZipArchiverShellTaskFailed = 1;
// This is important because the signals on `self` complete upon
// dealloc.
return:self]
ignoreValues]
concat:[RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
then:^{
return [RACSignal
zip:@[ self.taskTerminated, self.standardErrorData ]
reduce:^(NSNumber *exitStatus, NSData *errorData) {
if (exitStatus.intValue == 0) return [RACSignal empty];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
userInfo[SQRLZipArchiverExitCodeErrorKey] = exitStatus;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
NSString *errorString = [[NSString alloc] initWithData:errorData encoding:NSUTF8StringEncoding];
errorString = [errorString stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
if (errorString.length > 0) userInfo[NSLocalizedDescriptionKey] = errorString;
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}]]
return [RACSignal error:[NSError errorWithDomain:SQRLZipArchiverErrorDomain code:SQRLZipArchiverShellTaskFailed userInfo:userInfo]];
}];
}]
take:1]
flatten]
replay]
setNameWithFormat:@"-launchWithArguments: %@", arguments];
self.dittoTask.arguments = arguments;
-150
View File
@@ -1,150 +0,0 @@
//
// main.m
// shipit
//
// Created by Alan Rogers on 29/07/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.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 "SQRLShipItState.h"
#import "SQRLTerminationListener.h"
// The maximum number of times ShipIt should run the same installation state, in
// an attempt to update.
//
// If ShipIt is launched in the same state more than this number of times,
// updating will abort.
static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
// Waits for all instances of the target application (as described in the
// `state`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
return [[RACSignal
defer:^{
if (state.bundleIdentifier == nil) return [RACSignal empty];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
return [listener waitForTermination];
}]
setNameWithFormat:@"waitForTerminationIfNecessary"];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
NSLog(@"ShipIt quitting");
});
if (argc < 2) {
NSLog(@"Missing launchd job label for ShipIt");
return EXIT_FAILURE;
}
const char *jobLabel = argv[1];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@(jobLabel)];
RACSignal *stateLocation = directoryManager.shipItStateURL;
[[[[[[[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;
}
+170
View File
@@ -0,0 +1,170 @@
//
// main.m
// shipit
//
// Created by Alan Rogers on 29/07/2013.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLInstaller+Private.h"
#import "SQRLShipItRequest.h"
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
typedef NS_ENUM(NSInteger, SQRLShipItError) {
SQRLShipItErrorUnknownAction,
SQRLShipItErrorMissingRequestData,
};
static void reply(xpc_object_t response) {
xpc_connection_t connection = xpc_dictionary_get_remote_connection(response);
xpc_connection_send_message(connection, response);
}
static void replyWithError(xpc_object_t response, NSError *error) {
xpc_dictionary_set_bool(response, "success", false);
if (error != nil) {
xpc_object_t errorDictionary = xpc_dictionary_create(NULL, NULL, 0);
xpc_dictionary_set_string(errorDictionary, "domain", error.domain.UTF8String);
xpc_dictionary_set_int64(errorDictionary, "code", error.code);
xpc_dictionary_set_string(errorDictionary, "description", error.localizedDescription.UTF8String);
xpc_dictionary_set_string(errorDictionary, "failureReason", error.localizedFailureReason.UTF8String);
xpc_dictionary_set_string(errorDictionary, "recoverySuggestion", error.localizedRecoverySuggestion.UTF8String);
xpc_dictionary_set_value(response, "error", errorDictionary);
xpc_release(errorDictionary);
}
reply(response);
}
static void replyWithSuccess(xpc_object_t response) {
xpc_dictionary_set_bool(response, "success", true);
reply(response);
}
// Client requests from peer connections.
//
// applicationIdentifier - Current process reverse DNS identifier.
// request - XPC dictionary request object.
//
// Returns nothing.
static void handleRequest(NSString *applicationIdentifier, xpc_object_t request) {
xpc_object_t response = xpc_dictionary_create_reply(request);
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
char const *action = xpc_dictionary_get_string(request, "action");
if (strcmp(action, "install") != 0) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot process \"%s\" requests", action],
};
replyWithError(response, [NSError errorWithDomain:SQRLShipItErrorDomain code:SQRLShipItErrorUnknownAction userInfo:errorInfo]);
return;
}
size_t length;
void const *requestBytes = xpc_dictionary_get_data(request, "request", &length);
if (requestBytes == NULL) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Cannot process \"%s\" requests without \"request\" data", action],
};
replyWithError(response, [NSError errorWithDomain:SQRLShipItErrorDomain code:SQRLShipItErrorUnknownAction userInfo:errorInfo]);
return;
}
[[[SQRLShipItRequest
readFromData:[NSData dataWithBytes:requestBytes length:length] ]
flattenMap:^(SQRLShipItRequest *request) {
return [[installer.installUpdateCommand
execute:request]
catch:^(NSError *error) {
return [[[installer.abortInstallationCommand
execute:nil]
doCompleted:^{
NSLog(@"Abort completed successfully");
}]
concat:[RACSignal error:error]];
}];
}]
subscribeError:^(NSError *error){
NSLog(@"Installation failed with error: %@ %@", error, error.userInfo);
replyWithError(response, error);
} completed:^{
NSLog(@"Installation completed successfully");
replyWithSuccess(response);
}];
}
// Peer connections from the listener connection.
//
// applicationIdentifier - Current process reverse DNS identifier.
// newConnection - The newly accepted connection from the listener.
//
// Returns nothing.
static void handleConnection(NSString *applicationIdentifier, xpc_connection_t newConnection) {
xpc_connection_set_event_handler(newConnection, ^(xpc_object_t object) {
xpc_type_t type = xpc_get_type(object);
if (type == XPC_TYPE_ERROR) {
char *description = xpc_copy_description(object);
NSLog(@"XPC peer error: %s", description);
free(description);
return;
}
if (type != XPC_TYPE_DICTIONARY) {
NSLog(@"XPC peer expected dictionary");
return;
}
handleRequest(applicationIdentifier, object);
});
xpc_connection_resume(newConnection);
}
// Listens for XPC messages from clients.
//
// Arguments are expected in the following order:
//
// jobLabel - The launchd job label for this task.
//
// Returns 0 on successful termination, non 0 otherwise.
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
NSLog(@"ShipIt quitting");
});
if (argc < 2) {
NSLog(@"Missing launchd job label for ShipIt");
return EXIT_FAILURE;
}
char const *jobLabel = argv[1];
NSString *applicationIdentifier = @(jobLabel);
xpc_connection_t server = xpc_connection_create_mach_service(jobLabel, NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
xpc_connection_set_event_handler(server, ^(xpc_object_t object) {
xpc_type_t type = xpc_get_type(object);
if (type == XPC_TYPE_ERROR) {
char *description = xpc_copy_description(object);
NSLog(@"XPC listener error: %s", description);
free(description);
} else if (type == XPC_TYPE_CONNECTION) {
handleConnection(applicationIdentifier, object);
}
});
xpc_connection_resume(server);
dispatch_main();
}
return EXIT_SUCCESS;
}
@@ -30,11 +30,11 @@ it(@"should verify a valid bundle", ^{
it(@"should fail to verify with different code signing requirements", ^{
NSError *error = nil;
SQRLCodeSignature *verifier = [SQRLCodeSignature currentApplicationSignature:&error];
expect(verifier).notTo.beNil();
SQRLCodeSignature *signature = [SQRLCodeSignature currentApplicationSignature:&error];
expect(signature).notTo.beNil();
expect(error).to.beNil();
BOOL success = [[verifier verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
});
+102 -75
View File
@@ -9,22 +9,43 @@
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLInstaller+Private.h"
#import "SQRLShipItConnection.h"
#import "SQRLShipItRequest.h"
#import "SQRLInstallerOwnedBundle.h"
SpecBegin(SQRLInstaller)
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);
};
__block NSURL *updateURL;
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
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();
it(@"should install an update using ShipIt", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
expect(self.testApplicationBundleVersion).will.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).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
@@ -34,11 +55,9 @@ it(@"should install an update and relaunch", ^{
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier];
expect(apps.count).to.equal(0);
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();
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:YES];
[self launchShipIt];
[self installWithRequest:request remote:YES];
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count).will.equal(1);
@@ -48,10 +67,9 @@ it(@"should install an update from another volume", ^{
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication 2.1" fromDirectory:updateURL.URLByDeletingLastPathComponent];
updateURL = [diskImageURL URLByAppendingPathComponent:updateURL.lastPathComponent];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
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];
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
@@ -60,70 +78,80 @@ it(@"should install an update to another volume", ^{
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication" fromDirectory:self.testApplicationURL.URLByDeletingLastPathComponent];
NSURL *targetURL = [diskImageURL URLByAppendingPathComponent:self.testApplicationURL.lastPathComponent];
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:targetURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
expect([[state writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL]).to.beTruthy();
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
NSURL *plistURL = [targetURL URLByAppendingPathComponent:@"Contents/Info.plist"];
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
it(@"should install an update in process", ^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
state.installerState = SQRLInstallerStateClearingQuarantine;
describe(@"with backup restoration", ^{
__block NSURL *targetURL;
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithDirectoryManager:SQRLDirectoryManager.currentApplicationManager];
expect(installer).notTo.beNil();
__block SQRLShipItRequest *request;
NSError *installError = nil;
BOOL install = [[installer installUpdateWithState:state] asynchronouslyWaitUntilCompleted:&installError];
expect(install).to.beTruthy();
expect(installError).to.beNil();
beforeEach(^{
targetURL = self.testApplicationURL;
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO];
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();
});
afterEach(^{
__block NSError *error;
expect([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error]).will.beTruthy();
expect(error).to.beNil();
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
it(@"should not install an update after too many attempts", ^{
[self installWithRequest:request remote:YES];
});
it(@"should relaunch even after failing to install an update", ^{
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES];
[self installWithRequest:request remote:YES];
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count).will.equal(1);
});
});
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();
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);
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();
expect(modeOfURL(updateURL)).to.equal(0777);
expect(modeOfURL([updateURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0777);
[self launchShipIt];
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO];
// 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();
[self installWithRequest:request remote:YES];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
});
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
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);
expect(modeOfURL(self.testApplicationURL)).to.equal(0755);
expect(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"])).to.equal(0755);
});
describe(@"signal handling", ^{
@@ -134,13 +162,9 @@ describe(@"signal handling", ^{
// accessing the property.
targetURL = self.testApplicationURL;
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:self.testApplicationURL updateBundleURL:updateURL bundleIdentifier:nil codeSignature:self.testApplicationSignature];
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];
// 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);
[self installWithRequest:request remote:YES];
// Apply a random delay before sending the termination signal, to
// fuzz out race conditions.
@@ -154,33 +178,36 @@ describe(@"signal handling", ^{
Expecta.asynchronousTestTimeout = 5;
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
NSError *error = nil;
NSError *error;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should handle SIGHUP", ^{
system("killall -HUP ShipIt");
system("killall -HUP shipit-installer");
});
it(@"should handle SIGTERM", ^{
system("killall -TERM ShipIt");
system("killall -TERM shipit-installer");
});
it(@"should handle SIGINT", ^{
system("killall -INT ShipIt");
system("killall -INT shipit-installer");
});
it(@"should handle SIGQUIT", ^{
system("killall -QUIT ShipIt");
system("killall -QUIT shipit-installer");
});
it(@"should handle SIGKILL", ^{
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
// a few times to really test resumption.
// 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 < 3; i++) {
system("killall -KILL ShipIt");
system("killall -KILL shipit-installer");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
+79
View File
@@ -0,0 +1,79 @@
//
// SQRLShipItStateSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-09.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLDirectoryManager.h"
#import "SQRLShipItRequest.h"
SpecBegin(SQRLShipItRequest)
__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 when no file exists yet", ^{
NSError *error;
BOOL success = [[SQRLShipItRequest readFromURL:[directoryManager.shipItStateURL first]] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
});
it(@"should write and read to disk", ^{
NSError *error;
BOOL success = [[request writeToURL:[directoryManager.shipItStateURL first]] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readFromURL:[directoryManager.shipItStateURL first]] 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 readFromURL: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);
});
SpecEnd
-63
View File
@@ -1,63 +0,0 @@
//
// 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
@@ -1,68 +0,0 @@
//
// SQRLTerminationListenerSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-07.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTerminationListener.h"
SpecBegin(SQRLTerminationListener)
__block SQRLTerminationListener *listener;
beforeEach(^{
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
expect(listener).notTo.beNil();
});
it(@"should complete immediately when the app is not running", ^{
__block BOOL completed = NO;
[[listener waitForTermination] subscribeCompleted:^{
completed = YES;
}];
expect(completed).to.beTruthy();
});
it(@"should wait until one instance terminates", ^{
NSRunningApplication *app = [self launchTestApplicationWithEnvironment:nil];
__block NSRunningApplication *observedApp = nil;
__block BOOL completed = NO;
[[listener waitForTermination] subscribeNext:^(id x) {
observedApp = x;
} completed:^{
completed = YES;
}];
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
[app forceTerminate];
expect(app.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
it(@"should wait until multiple instances terminate", ^{
NSRunningApplication *app1 = [self launchTestApplicationWithEnvironment:nil];
NSRunningApplication *app2 = [self launchTestApplicationWithEnvironment:nil];
__block BOOL completed = NO;
[[listener waitForTermination] subscribeCompleted:^{
completed = YES;
}];
expect(completed).to.beFalsy();
[app1 forceTerminate];
expect(app1.terminated).will.beTruthy();
expect(completed).to.beFalsy();
[app2 forceTerminate];
expect(app2.terminated).will.beTruthy();
expect(completed).will.beTruthy();
});
SpecEnd
+6 -7
View File
@@ -18,8 +18,9 @@ extern NSString * const SQRLBundleShortVersionStringKey;
@class SQRLCodeSignature;
@class SQRLDirectoryManager;
@class SQRLShipItRequest;
@interface SQRLTestCase : SPTSenTestCase
@interface SQRLTestCase : SPTXCTestCase
// A URL to a temporary directory tests can use.
//
@@ -45,10 +46,6 @@ 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;
@@ -74,8 +71,10 @@ extern NSString * const SQRLBundleShortVersionStringKey;
// deleted at the end of the example.
- (NSURL *)createTestApplicationUpdate;
// Submits ShipIt's launchd job to start it up.
- (void)launchShipIt;
// Runs the installer in process or submits ShipIt's launchd job to start it up.
//
// request - The install to send to ShipIt.
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
// Creates a disk image, then mounts it.
//
+75 -77
View File
@@ -7,9 +7,13 @@
//
#import "SQRLTestCase.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItConnection.h"
#import "SQRLInstaller.h"
#import "SQRLShipItRequest.h"
#import "SQRLTestHelper.h"
#import <ServiceManagement/ServiceManagement.h>
#pragma clang diagnostic push
@@ -53,7 +57,7 @@ static void SQRLSignalHandler(int sig) {
// Returns an _unlaunched_ task that will follow log files at the given paths,
// then pipe that output through this process.
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths;
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths;
@end
@@ -75,24 +79,23 @@ static void SQRLSignalHandler(int sig) {
@"otest-x86_64.ShipIt",
];
RACSignal *logLocations = [[[[folders.rac_signal
flattenMap:^(NSString *folder) {
return [folder stringsByAppendingPaths:@[
@"ShipIt_stdout.log",
@"ShipIt_stderr.log",
]].rac_signal;
}]
map:^(NSString *path) {
return [appSupportURL URLByAppendingPathComponent:path];
}]
doNext:^(NSURL *location) {
[[NSData data] writeToURL:location atomically:YES];
}]
map:^(NSURL *location) {
return location.path;
}];
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
NSURL *baseURL = [appSupportURL URLByAppendingPathComponent:folder];
return @[
[baseURL URLByAppendingPathComponent:@"ShipIt_stdout.log"],
[baseURL URLByAppendingPathComponent:@"ShipIt_stderr.log"]
].rac_sequence;
}];
NSTask *readShipIt = [self tailTaskWithPaths:logLocations];
for (NSURL *URL in URLs) {
[[NSData data] writeToURL:URL atomically:YES];
}
RACSequence *paths = [URLs map:^(NSURL *URL) {
return URL.path;
}];
NSTask *readShipIt = [self tailTaskWithPaths:paths];
[readShipIt launch];
NSAssert([readShipIt isRunning], @"Could not start task %@", readShipIt);
@@ -111,20 +114,6 @@ static void SQRLSignalHandler(int sig) {
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];
@@ -132,12 +121,12 @@ static void SQRLSignalHandler(int sig) {
}
- (void)addCleanupBlock:(dispatch_block_t)block {
[self.exampleCleanupBlocks addObject:[block copy]];
[SQRLTestHelper addCleanupBlock:block];
}
#pragma mark Logging
+ (NSTask *)tailTaskWithPaths:(RACSignal *)paths {
+ (NSTask *)tailTaskWithPaths:(RACSequence *)paths {
NSPipe *outputPipe = [NSPipe pipe];
NSFileHandle *outputHandle = outputPipe.fileHandleForReading;
@@ -159,10 +148,10 @@ static void SQRLSignalHandler(int sig) {
if (_baseTemporaryDirectoryURL == nil) {
NSURL *globalTemporaryDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
_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];
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
[self addCleanupBlock:^{
[NSFileManager.defaultManager removeItemAtURL:_baseTemporaryDirectoryURL error:NULL];
@@ -178,7 +167,7 @@ static void SQRLSignalHandler(int sig) {
NSError *error = nil;
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:temporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", temporaryDirectoryURL, error);
return temporaryDirectoryURL;
}
@@ -189,19 +178,19 @@ static void SQRLSignalHandler(int sig) {
NSURL *fixtureURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app" isDirectory:YES];
if (![NSFileManager.defaultManager fileExistsAtPath:fixtureURL.path]) {
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
NSError *error = nil;
BOOL success = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:fixtureURL error:&error];
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", bundleURL, fixtureURL, error);
NSURL *testAppLog = [fixtureURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:@"TestApplication.log"];
[[NSData data] writeToURL:testAppLog atomically:YES];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSignal return:testAppLog.path]];
NSTask *readTestApp = [self.class tailTaskWithPaths:[RACSequence return:testAppLog.path]];
[readTestApp launch];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
XCTAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
[readTestApp terminate];
@@ -214,8 +203,8 @@ static void SQRLSignalHandler(int sig) {
- (NSBundle *)testApplicationBundle {
NSURL *fixtureURL = self.testApplicationURL;
NSBundle *bundle = [NSBundle bundleWithURL:fixtureURL];
STAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
XCTAssertNotNil(bundle, @"Couldn't open bundle at %@", fixtureURL);
return bundle;
}
@@ -234,7 +223,7 @@ static void SQRLSignalHandler(int sig) {
NSError *error = nil;
NSRunningApplication *app = [NSWorkspace.sharedWorkspace launchApplicationAtURL:self.testApplicationURL options:NSWorkspaceLaunchWithoutAddingToRecents | NSWorkspaceLaunchWithoutActivation | NSWorkspaceLaunchNewInstance | NSWorkspaceLaunchAndHide configuration:configuration error:&error];
STAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
XCTAssertNotNil(app, @"Could not launch app at %@: %@", self.testApplicationURL, error);
[self addCleanupBlock:^{
if (!app.terminated) {
@@ -255,11 +244,11 @@ static void SQRLSignalHandler(int sig) {
- (NSURL *)createTestApplicationUpdate {
NSURL *originalURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication 2.1" withExtension:@"app"];
STAssertNotNil(originalURL, @"Couldn't find TestApplication update in test bundle");
XCTAssertNotNil(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];
STAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
XCTAssertNotNil(updateParentURL, @"Could not create temporary directory for updating: %@", error);
[self addCleanupBlock:^{
[NSFileManager.defaultManager removeItemAtURL:updateParentURL error:NULL];
@@ -267,18 +256,18 @@ static void SQRLSignalHandler(int sig) {
NSURL *updateURL = [updateParentURL URLByAppendingPathComponent:originalURL.lastPathComponent isDirectory:YES];
BOOL success = [NSFileManager.defaultManager copyItemAtURL:originalURL toURL:updateURL error:&error];
STAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
XCTAssertTrue(success, @"Couldn't copy %@ to %@: %@", originalURL, updateURL, error);
return updateURL;
}
- (id)performWithTestApplicationRequirement:(id (^)(SecRequirementRef requirement))block {
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
STAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
SecStaticCodeRef staticCode = NULL;
OSStatus status = SecStaticCodeCreateWithPath((__bridge CFURLRef)bundleURL, kSecCSDefaultFlags, &staticCode);
STAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
XCTAssertTrue(status == noErr, @"Error creating static code object for %@", bundleURL);
@onExit {
if (staticCode != NULL) CFRelease(staticCode);
@@ -286,7 +275,7 @@ static void SQRLSignalHandler(int sig) {
SecRequirementRef requirement = NULL;
status = SecCodeCopyDesignatedRequirement(staticCode, kSecCSDefaultFlags, &requirement);
STAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
XCTAssertTrue(status == noErr, @"Error getting designated requirement of %@", staticCode);
@onExit {
if (requirement != NULL) CFRelease(requirement);
@@ -296,48 +285,57 @@ static void SQRLSignalHandler(int sig) {
}
- (SQRLCodeSignature *)testApplicationSignature {
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
return [[SQRLCodeSignature alloc] initWithRequirement:requirement];
}];
}
NSURL *bundleURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication" withExtension:@"app"];
XCTAssertNotNil(bundleURL, @"Couldn't find TestApplication.app in test bundle");
- (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);
NSError *error = nil;
SQRLCodeSignature *signature = [SQRLCodeSignature signatureWithBundle:bundleURL error:&error];
XCTAssertNotNil(signature, @"Error getting signature for bundle at %@: %@", bundleURL, error);
return CFBridgingRelease(data);
}];
return signature;
}
- (SQRLDirectoryManager *)shipItDirectoryManager {
NSString *identifier = SQRLShipItLauncher.shipItJobLabel;
NSString *identifier = SQRLShipItConnection.shipItJobLabel;
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:identifier];
STAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
XCTAssertNotNil(manager, @"Could not create directory manager for %@", identifier);
return manager;
}
- (void)launchShipIt {
NSError *error = nil;
STAssertTrue([[SQRLShipItLauncher launchPrivileged:NO] waitUntilCompleted:&error], @"Could not launch ShipIt: %@", error);
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote {
if (remote) {
SQRLShipItConnection *connection = [[SQRLShipItConnection alloc] initWithRootPrivileges:NO];
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
__block NSError *error = nil;
expect([[connection sendRequest:request] waitUntilCompleted:&error]).to.beTruthy();
expect(error).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];
}];
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItConnection.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();
}
}
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL {
NSURL *destinationURL = [self.baseTemporaryDirectoryURL URLByAppendingPathComponent:name];
STAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
XCTAssertNotNil(destinationURL, @"Could not create disk image URL for %@", directoryURL);
NSString *createInvocation;
if (directoryURL == nil) {
+17
View File
@@ -0,0 +1,17 @@
//
// SQRLTestHelper.h
// Squirrel
//
// Created by Matt Diephouse on 7/7/14.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
/// A test helper class that will run cleanup blocks after each test finishes.
@interface SQRLTestHelper : NSObject
/// Adds a cleanup block that will run after the next test completes.
+ (void)addCleanupBlock:(dispatch_block_t)block;
@end
+42
View File
@@ -0,0 +1,42 @@
//
// SQRLTestHelper.m
// Squirrel
//
// Created by Matt Diephouse on 7/7/14.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLTestHelper.h"
#import <objc/runtime.h>
@implementation SQRLTestHelper
#pragma mark - Specta Methods
+ (void)afterEach {
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in self.cleanupBlocks.reverseObjectEnumerator) {
block();
}
[self.cleanupBlocks removeAllObjects];
}
#pragma mark - Public Methods
+ (void)addCleanupBlock:(dispatch_block_t)block {
[self.cleanupBlocks addObject:[block copy]];
}
#pragma mark - Private Methods
+ (NSMutableArray *)cleanupBlocks {
NSMutableArray *blocks = objc_getAssociatedObject(self, "sqrl_cleanupBlocks");
if (blocks == nil) {
blocks = [NSMutableArray array];
objc_setAssociatedObject(self, "sqrl_cleanupBlocks", blocks, OBJC_ASSOCIATION_RETAIN);
}
return blocks;
}
@end
+63 -10
View File
@@ -11,6 +11,7 @@
#import "SQRLTestUpdate.h"
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "TestAppConstants.h"
SpecBegin(SQRLUpdater)
@@ -154,7 +155,7 @@ describe(@"updating", ^{
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
if (contents == nil) return [RACSignal empty];
return contents.rac_signal;
return contents.rac_sequence.signal;
}]
filter:^(NSURL *directoryURL) {
return [directoryURL.lastPathComponent hasPrefix:@"update."];
@@ -177,13 +178,7 @@ describe(@"updating", ^{
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
// Also test that this is the only update directory (any others
// should've been removed).
NSArray *directoryURLs = [updateDirectoryURLs array];
expect(directoryURLs.count).to.equal(1);
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:directoryURLs[0] includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsHiddenFiles error:NULL];
expect(contents).to.equal(@[]);
expect([updateDirectoryURLs toArray]).will.equal(@[]);
});
});
});
@@ -208,7 +203,7 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(result).to.beFalsy();
expect(error.domain).to.equal(SQRLUpdaterErrorDomain);
expect(error.code).to.equal(SQRLUpdaterErrorInvalidServerResponse);
@@ -225,11 +220,69 @@ describe(@"response handling", ^{
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesAction signalWithValue:nil] asynchronouslyWaitUntilCompleted:&error];
BOOL result = [[updater.checkForUpdatesCommand execute:nil] 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]
map:^(NSNotification *notification) {
return notification.userInfo[SQRLTestAppUpdaterStateKey];
}]
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];
}];
NSRunningApplication *testApplication = launchWithEnvironment(nil);
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateIdle),
];
expect(states).will.equal(expectedStates);
expect(testApplication.terminated).will.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).will.equal(expectedStates);
expect(testApplication.terminated).will.beTruthy();
});
});
SpecEnd
+4 -1
View File
@@ -8,7 +8,7 @@
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SenTestingKit/SenTestingKit.h>
#import <XCTest/XCTest.h>
#import <Mantle/Mantle.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
@@ -21,5 +21,8 @@
#define EXP_SHORTHAND
#import "Expecta.h"
// Expecta #defines startWith as beginWith, which messes up -startWith: :-(
#undef startWith
#import "SQRLTestCase.h"
#endif
+22
View File
@@ -0,0 +1,22 @@
//
// 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
@@ -0,0 +1,13 @@
//
// 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";
+14 -7
View File
@@ -8,10 +8,11 @@
#import "TestAppDelegate.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItConnection.h"
#import "SQRLTestUpdate.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "TestAppConstants.h"
@interface TestAppDelegate ()
@@ -34,7 +35,7 @@
NSLog(@"TestApplication quitting");
});
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItConnection.shipItJobLabel];
NSError *error = nil;
BOOL removed = [[[directoryManager
@@ -63,6 +64,12 @@
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
@@ -70,18 +77,18 @@
NSLog(@"***** UPDATE CHECK %lu *****", (unsigned long)updateCheckCount);
updateCheckCount++;
return [self.updater.checkForUpdatesAction signalWithValue:nil];
return [self.updater.checkForUpdatesCommand execute:RACUnit.defaultUnit];
}]
doNext:^(SQRLDownloadedUpdate *update) {
NSLog(@"Got a candidate update: %@", update);
}]
// Retry until we get the expected release.
repeat]
skipWhile:^ BOOL (SQRLDownloadedUpdate *download) {
skipUntilBlock:^(SQRLDownloadedUpdate *download) {
SQRLTestUpdate *testUpdate = (id)download.update;
NSAssert([testUpdate isKindOfClass:SQRLTestUpdate.class], @"Unexpected update type: %@", testUpdate);
return !testUpdate.final;
return testUpdate.final;
}]
take:1]
doNext:^(id _) {
@@ -92,12 +99,12 @@
NSLog(@"Error in updater: %@", error);
return [RACSignal empty];
}]
concat:[RACSignal defer:^{
then:^{
NSString *delayString = NSProcessInfo.processInfo.environment[@"SQRLUpdateDelay"];
if (delayString == nil) return [RACSignal empty];
return [[RACSignal interval:delayString.doubleValue onScheduler:RACScheduler.mainThreadScheduler] take:1];
}]]
}]
subscribeCompleted:^{
[NSApp terminate:self];
}];