473 Commits
Author SHA1 Message Date
Josh Abernathy 5396dcd1f9 Merge pull request #210 from Squirrel/provide-more-error-info
Provide more error context when we get POSIX errors
2017-07-20 09:47:13 -04:00
Josh Abernathy 203315f93e Merge pull request #207 from Squirrel/escalators-are-dangerous
Fix installing updates which need escalated privileges
2017-07-20 09:46:47 -04:00
joshaber 523824ea57 Provide more error context when we get POSIX errors
Fixes #209
2017-07-20 08:59:57 -04:00
joshaber 306443aaaf Merge branch 'master' into escalators-are-dangerous 2017-07-19 12:39:31 -04:00
Josh Abernathy db17e6865f Merge pull request #208 from Squirrel/make-tests-happen
Make CI happen
2017-07-19 12:39:01 -04:00
joshaber d14376c277 Don't run tests 2017-07-19 11:51:24 -04:00
joshaber 70cdda1ea2 Update the fixture app 2017-07-18 17:26:43 -04:00
joshaber 168fc856ff Update the expected error 2017-07-18 17:26:43 -04:00
joshaber f83f4ddcb7 I guess we need to do this to make signing work? 2017-07-18 17:26:43 -04:00
joshaber 7d02f612f8 Merge branch 'master' into escalators-are-dangerous 2017-07-18 13:47:56 -04:00
Josh Abernathy 0d4d0ba602 Merge pull request #206 from Squirrel/update-xcode
Update to Xcode 8
2017-07-18 13:47:33 -04:00
joshaber 6f16206cef I dunno try this 2017-07-18 13:02:21 -04:00
joshaber ee89952cc4 Revert "I dunno try this"
This reverts commit 6b98348ceb.
2017-07-18 13:01:43 -04:00
joshaber 6b98348ceb I dunno try this 2017-07-18 13:01:30 -04:00
joshaber b63125bfe0 Don't need this import either 2017-07-18 12:52:13 -04:00
joshaber 5ca2018292 Hardcode this 2017-07-18 11:20:03 -04:00
joshaber bbb1edb0fb Separate building and testing 2017-07-18 10:47:50 -04:00
joshaber b98c5981af Use xcodebuild to build 2017-07-18 09:58:57 -04:00
joshaber 13c6951b35 Consume the ShipIt state path from the args 2017-07-17 17:34:05 -04:00
joshaber 8806258058 Pass the state path to ShipIt 2017-07-17 17:33:52 -04:00
joshaber e672a408f4 Merge branch 'update-xcode' into fix-permissions 2017-07-17 17:05:19 -04:00
joshaber 386b7c09af Update the Travis image too 2017-07-17 17:04:13 -04:00
joshaber 27dedad85d Check writability for the parent directory too
If the parent isn't writable (e.g., /Applications), we'll need to launch with escalated privileges.
2017-07-17 17:04:00 -04:00
joshaber c22ad09d8e Update our dependencies 2017-07-17 16:51:09 -04:00
joshaber 9f87ff2113 Update the project and schemes 2017-07-17 16:45:23 -04:00
Josh Abernathy bde5ff2983 Merge pull request #190 from slyngbaek/master
Add auth headers to zip download requests not just feed requests.
2016-11-08 10:38:28 -05:00
Steffen Lyngbaek ae78589d3a Update block type name 2016-11-07 14:56:36 -08:00
Steffen Lyngbaek abec74ed3f Add block to initializer to provide custom download requests.
Add a second initializer with with a block argument to handle
customization of download url requests.

Add a generic download request to the original initializer without
changing the current api.
2016-11-07 12:19:02 -08:00
Steffen Lyngbaek a994876d98 Don't clobber headers explicitly set on the zip download request. 2016-11-04 10:32:54 -07:00
Steffen Lyngbaek 9d5464c693 Add auth headers to zip download requests not just feed requests. 2016-11-03 14:14:51 -07:00
Keith Duncan f00ad7e91b Merge pull request #186 from Squirrel/dont-check-for-updates-when-readonly
Error if there's an update available but the app's on a read-only volume
2016-10-18 11:03:53 +11:00
joshaber 4b0d5ffe18 Error check the statfs call 2016-10-17 14:04:20 -04:00
joshaber d93abe66ec Use a better description 2016-10-17 14:04:09 -04:00
joshaber 196abf93b8 Use a unique error code 2016-10-17 14:03:59 -04:00
joshaber a0079fdee0 Only error once we know if it matters 2016-10-14 10:47:24 -04:00
joshaber fdb4ffa000 Throw an error instead of silently failing 2016-10-14 10:43:56 -04:00
joshaber cc047bdc6f Get outta town if the volume's readonly 2016-10-14 10:43:09 -04:00
joshaber 7231919408 Added -isRunningOnReadOnlyVolume 2016-10-14 10:40:33 -04:00
Josh Abernathy e9e2188cda Merge pull request #176 from Squirrel/fix-updates-again
Don't error if our conditional GET fails.
2016-06-15 08:05:34 -07:00
joshaber 413b1d2bca Reorganize to separate out the unarchiving 2016-06-14 15:37:24 -07:00
joshaber d92d385431 Don't error if our conditional GET fails. 2016-06-13 18:09:24 -07:00
Josh Abernathy 9ebfd397fd Merge pull request #175 from Squirrel/fix-pruning
Don't prune if we've already downloaded updates.
2016-06-12 11:28:48 -04:00
joshaber 7dffc4b143 Don't prune if we've already downloaded updates.
Fixes #174
2016-06-08 14:30:37 -04:00
Josh Abernathy 37cd028730 Merge pull request #172 from Squirrel/truncate-logs
Truncate logs
2016-06-03 12:43:44 -04:00
joshaber cc8993e6e7 Bump up the max length. 2016-06-03 09:55:05 -04:00
joshaber a0b35f490b Revert "Move back to the beginning of the file before writing."
This reverts commit ed0614d8cf.
2016-06-02 12:32:29 -04:00
joshaber ed0614d8cf Move back to the beginning of the file before writing. 2016-06-02 12:31:03 -04:00
joshaber ea70eb937a Truncate log files. 2016-06-02 12:27:21 -04:00
joshaber b5ecff7e03 Use the log paths instead of constructing them manually. 2016-06-02 12:27:11 -04:00
joshaber 8589cbedbb Expose the log paths. 2016-06-02 12:27:02 -04:00
Josh Abernathy edc604bfec Merge pull request #171 from Squirrel/stop-redownloading
Conditionally download updates.
2016-06-02 10:13:22 -04:00
Keith Duncan 8995e0faeb Merge pull request #169 from Squirrel/use-cache
Use the Caches directory instead of Application Support.
2016-06-02 16:07:17 +02:00
joshaber 67a68bef8c 🍩 need this change 2016-06-01 14:26:06 -04:00
joshaber 7c4b43d2d1 Conditionally download the update. 2016-06-01 14:20:04 -04:00
Josh Abernathy 694f96abec More words.
Fixes #120.
2016-06-01 13:49:01 -04:00
joshaber 369854b103 Update specs 2016-06-01 13:12:24 -04:00
joshaber 104ebb9442 s/an/a 2016-06-01 12:59:49 -04:00
joshaber e1f56bfdc6 Use the Caches directory instead of Application Support. 2016-06-01 12:55:24 -04:00
Keith Duncan 4a50411246 Merge pull request #165 from adam-lynch/patch-1
README: removing trailing comma from JSON format
2016-02-22 10:06:24 +11:00
Adam Lynch fc3ce8158c README: removing trailing comma from JSON format 2016-02-21 22:54:01 +00:00
Josh Abernathy 6e1e11809e Merge pull request #156 from Squirrel/dates
Fix parsing of ISO 8601 dates
2015-11-09 13:25:16 -05:00
Matt Diephouse 9d67b82d5c Use the correct day specifier
`DD` is day of year; `dd` is day of month.
2015-11-09 12:58:40 -05:00
Matt Diephouse 09a3b74f93 Test the dates are correct 2015-11-09 12:57:41 -05:00
Matt Diephouse c108f4aac4 Merge pull request #154 from Squirrel/fix-ci
Fix CI
2015-10-26 17:43:39 -04:00
joshaber 0ac595efd9 I am dissappoint, Travis 2015-10-26 17:25:51 -04:00
joshaber ce1b59c1cf Revert "Smore random sleeps"
This reverts commit 2dea99045d.
2015-10-26 17:17:55 -04:00
joshaber f7438e91b3 Revert "More sleeping!"
This reverts commit 656d2cdd29.
2015-10-26 17:17:46 -04:00
joshaber 656d2cdd29 More sleeping! 2015-10-26 17:08:03 -04:00
joshaber d8a244dd1a Try Xcode 7.1 for funsies. 2015-10-26 16:58:21 -04:00
joshaber 2dea99045d Smore random sleeps 2015-10-26 16:56:28 -04:00
joshaber 483eb38d73 Resource rules were deprecated. 2015-10-26 16:55:30 -04:00
joshaber 475c50a8e3 🔥 mdeep-codesign
It hasn’t been needed for a while.
2015-10-26 16:51:37 -04:00
joshaber 4384ccb485 I dunno let's try waiting. 2015-10-26 16:46:29 -04:00
joshaber 4b4bb7c87c Stop tailing 2015-10-26 16:36:46 -04:00
joshaber cd6ccb9c7b Try just one test. 2015-10-26 16:27:11 -04:00
joshaber 0c91d7fe9c Don't use async anymore. 2015-10-26 16:27:06 -04:00
joshaber 0d8fe0f642 LOL 2015-10-26 16:13:15 -04:00
joshaber b6f4e70731 Try an async. 2015-10-26 16:09:25 -04:00
joshaber efd944e337 Revert "can u not"
This reverts commit 5bfd32eeae.
2015-10-26 16:02:03 -04:00
joshaber cc86db04e2 Standardize on our longer timeout. 2015-10-26 16:01:45 -04:00
joshaber c9ce161632 Up the timeout. 2015-10-26 14:08:08 -04:00
joshaber f924fc3023 Make sure we don't use automatic termination. 2015-10-26 13:58:19 -04:00
joshaber 5bfd32eeae can u not 2015-10-26 13:26:54 -04:00
joshaber 7c365d304f Ugh spelling. 2015-10-26 13:06:31 -04:00
joshaber a6a782aefb Bigger timeout? 2015-10-26 12:59:31 -04:00
joshaber aa72c42c3b Setup some certs. 2015-10-26 12:21:33 -04:00
joshaber bee2be5dfb Revert "🔥 Deep code sign."
This reverts commit d0c932a90d.
2015-10-26 11:58:05 -04:00
joshaber 9bcc86be72 Update Travis again. 2015-10-26 11:42:43 -04:00
joshaber d0c932a90d 🔥 Deep code sign.
It’s been unnecessary for a while now, afaik. /cc @keithduncan
2015-10-26 11:41:40 -04:00
joshaber 4a0aa59d25 We really don't care about app extensions. 2015-10-26 11:40:49 -04:00
joshaber 70a838c834 Use Travis. 2015-10-23 16:52:54 -04:00
joshaber 19dc11c735 Fix the tests. 2015-10-23 16:52:50 -04:00
Matt Diephouse 87e854e8d4 Merge pull request #153 from Squirrel/xcode-7
Xcode 7 support
2015-10-23 10:45:12 -04:00
joshaber 5640bbe863 --regret 2015-10-23 09:49:49 -04:00
joshaber 843ee9fa22 Revert "Update all our references to RAC files."
This reverts commit 0026cef7bf.
2015-10-23 09:44:20 -04:00
joshaber 95ce084ddd Revert "Go with the swift2 branch of RAC."
This reverts commit 2cc8a7d4ab.
2015-10-23 09:44:12 -04:00
joshaber ed92f6aad2 Revert "Bleh."
This reverts commit 56f52ccfe4.
2015-10-23 09:44:02 -04:00
joshaber 56f52ccfe4 Bleh. 2015-10-22 17:19:17 -04:00
joshaber 2cc8a7d4ab Go with the swift2 branch of RAC. 2015-10-22 17:07:23 -04:00
joshaber 0026cef7bf Update all our references to RAC files. 2015-10-22 17:03:34 -04:00
joshaber f0fee4a4e8 ++RAC submodule 2015-10-22 16:41:45 -04:00
joshaber 1898bbde62 ++more things 2015-10-22 16:40:20 -04:00
joshaber 031a7cd850 ++everything 2015-10-22 16:31:59 -04:00
joshaber 27f7777ba5 Hi Xcode 7. 2015-10-22 16:17:57 -04:00
Matt Diephouse 8caa2fa200 Merge pull request #152 from Squirrel/fix-pruning
Fix pruning
2015-09-22 10:40:54 -05:00
joshaber 2fe45e940f Just use a method. 2015-09-22 00:21:23 +01:00
joshaber e50720470c Don't replay, that's crazy.
If we replay, then we don’t ever prune after the first subscription. We
just keep replaying the previous results.

Fixes https://github.com/atom/atom/issues/7061.
2015-09-21 00:17:11 +01:00
joshaber cebf4a85d6 Failing test. 2015-09-21 00:14:25 +01:00
joshaber 5f96d1e565 Allow tests to specify the number of update requests. 2015-09-21 00:14:08 +01:00
joshaber 1f3e0ccbe5 Verify that we have updates. 2015-09-21 00:10:30 +01:00
Keith Duncan 6252f3dbda Merge pull request #149 from Squirrel/renames
Renames
2015-06-29 17:09:59 +10:00
joshaber 1e71daa4f7 Merge branch 'renames-rebase' into renames
Conflicts:
	Squirrel/SQRLInstaller.m
2015-06-23 15:32:15 -04:00
joshaber 8eb2517d6b Do the rename first. 2015-06-23 15:30:58 -04:00
joshaber 46e044726f Do the rename first. 2015-06-23 14:10:39 -04:00
joshaber b30ee65933 Be more explicit. 2015-06-22 11:41:32 -04:00
joshaber ae89c0a1f6 Be more explicit. 2015-06-22 11:24:24 -04:00
joshaber 54f765b19e Naming and docs. 2015-06-19 15:30:33 -04:00
joshaber d8a1185c6b Let the abort use the old request. 2015-06-19 15:30:27 -04:00
joshaber dbf430683f Fix the tests. 2015-06-19 15:30:21 -04:00
joshaber d0634fc8e6 Naming and docs. 2015-06-19 13:48:31 -04:00
joshaber 178648f373 Let the abort use the old request. 2015-06-19 12:47:03 -04:00
joshaber 3a4428cfb3 Fix the tests. 2015-06-19 12:35:28 -04:00
joshaber dab5bf2892 Merge branch 'renames-rebase' into renames
Conflicts:
	Squirrel/SQRLInstaller.m
2015-06-19 12:31:46 -04:00
joshaber e3aa7a6767 Only rename if needed. 2015-06-19 12:21:42 -04:00
joshaber 0c8394f6f4 Rename after moving. 2015-06-19 12:08:55 -04:00
joshaber 6779322eea Deliver an updated request. 2015-06-19 11:00:07 -04:00
joshaber 5d8ffc9070 Use -installItemToURL:fromURL:. 2015-06-18 16:23:51 -04:00
joshaber 72345d4c18 Serialize whether we're allowed to rename. 2015-06-18 15:58:55 -04:00
joshaber 52a6dc2fd2 More logging. 2015-06-18 15:28:13 -04:00
joshaber 5b05c23877 Only do the dance when installing. 2015-06-18 15:18:47 -04:00
joshaber 81c0841201 Logging. 2015-06-18 14:33:09 -04:00
joshaber 747e183b98 Check the bundle ID at our new destination. 2015-06-18 14:10:03 -04:00
joshaber 62a39671fc Early exits. 2015-06-18 14:09:57 -04:00
joshaber 3be9c89f49 Use the source's name as the new name. 2015-06-18 14:09:52 -04:00
joshaber ceb4f34827 RAC em up. 2015-06-18 14:09:46 -04:00
joshaber 22e785a637 Try the dumb guy route first. 2015-06-18 14:09:41 -04:00
joshaber 221519e3ad Check the bundle ID at our new destination. 2015-06-18 13:58:38 -04:00
joshaber 7faed9ec18 Early exits. 2015-06-18 13:58:29 -04:00
joshaber c74ecf9c07 Use the source's name as the new name. 2015-06-18 13:47:47 -04:00
joshaber 04233f8f7c RAC em up. 2015-06-18 13:34:27 -04:00
joshaber 378e3891bf Try the dumb guy route first. 2015-06-18 12:27:59 -04:00
Justin Spahr-Summers 9e040347f5 Merge pull request #145 from Squirrel/xcode6.3
Upgrade dependencies for Xcode 6.3
2015-04-09 16:06:29 -07:00
Justin Spahr-Summers c99ce7dfbb Upgrade dependencies for Xcode 6.3 2015-04-09 14:59:58 -07:00
Justin Spahr-Summers 6b017d5d3f Fix call to superclass method that doesn't exist
Not sure how this ever worked.
2015-04-09 14:56:04 -07:00
Justin Spahr-Summers b1f4ecf615 Merge pull request #143 from Squirrel/quote-paths-when-signing
Quote the paths passed to otool.
2015-02-12 12:15:42 -08:00
Rob Rix fd29bc392f Quote the paths passed to otool.
We pass in a path which could contain spaces, so we need to quote it.
2015-02-12 10:02:32 -05:00
Justin Spahr-Summers 61b5ceb1cb Merge pull request #142 from Squirrel/pacify-xcode
Pacify Xcode
2015-01-16 13:50:10 -08:00
joshaber 38a8914b55 OK Computer 2015-01-16 15:06:06 -05:00
joshaber ede35f29d8 ++xcconfigs 2015-01-16 15:05:50 -05:00
Justin Spahr-Summers 7e8abd13c4 Merge pull request #141 from Squirrel/keithduncan-patch-1
Separate the update available and install instructions
2015-01-15 12:21:37 -08:00
Keith Duncan d776817d8d More tweaks 2015-01-15 12:18:34 -08:00
Keith Duncan 82d79b65c3 Tweak wording 2015-01-15 11:40:26 -08:00
Keith Duncan 8cf49c6fef Separate the update available and install instructions 2015-01-14 14:32:40 -08:00
Justin Spahr-Summers caba5b81af Merge pull request #140 from Squirrel/sign-shipit
Let Xcode code sign ShipIt.
2015-01-10 10:01:34 -08:00
joshaber 98efd568df Let Xcode code sign ShipIt. 2015-01-09 17:58:31 -08:00
Rob Rix 0e729b3725 Merge pull request #139 from Squirrel/carthage-0.5
Migrate to Carthage 0.5
2014-12-30 02:07:23 -05:00
Justin Spahr-Summers 190ac7a2b0 Bump Cartfile 2014-12-29 21:13:42 -08:00
Justin Spahr-Summers 7e91d04b28 Use a Cartfile.private, loosen version constraints 2014-12-29 16:25:54 -08:00
Justin Spahr-Summers c43df8037b Update to Carthage 0.5 directory structure 2014-12-29 16:24:28 -08:00
Matt Diephouse 5471b65a4a Merge pull request #136 from Squirrel/bump-rac
Bump Carthage dependencies
2014-12-08 13:36:12 -05:00
Justin Spahr-Summers 37d3b3f802 Add extobjc header paths to Squirrel target 2014-12-08 10:26:24 -08:00
Justin Spahr-Summers bd9abf504d Bump Carthage dependencies (particularly RAC)
Hopefully resolves #118.
2014-12-08 10:17:14 -08:00
Keith Duncan f415391772 Merge pull request #135 from Squirrel/skip-cstemp
Don’t attempt to sign .cstemp files
2014-12-05 21:12:14 +00:00
Rob Rix c29fee075d Delete .cstemp files just prior to codesigning. 2014-12-05 14:40:21 -05:00
Rob Rix bcd5433733 Revert "Don’t attempt to sign .cstemp files"
This reverts commit c0df5b1d57.
2014-12-05 14:36:14 -05:00
Rob Rix c0df5b1d57 Don’t attempt to sign .cstemp files 2014-12-03 11:12:14 -05:00
Justin Spahr-Summers f61a35ab42 Merge pull request #133 from Squirrel/cartfile
Use Carthage for managing submodules
2014-11-21 15:39:03 -08:00
Justin Spahr-Summers 0575de80c6 Use Carthage for managing submodules 2014-11-21 15:36:02 -08:00
Josh Abernathy 2a60c9c0f3 Merge pull request #132 from Squirrel/xcode6
Xcode 6.1 support
2014-11-07 14:45:51 -05:00
Justin Spahr-Summers d9fe18f95e Actually wait for the backup to be restored :( 2014-11-07 11:33:38 -08:00
Justin Spahr-Summers c7ea0aedb8 Always create the directory for test log files 2014-11-07 11:19:36 -08:00
Justin Spahr-Summers f340c9cb41 Remove deprecated Debug key for launchd.plist 2014-11-07 11:09:22 -08:00
Justin Spahr-Summers 8babde1e71 Fix ShipIt job label for tests 2014-11-07 11:09:06 -08:00
Justin Spahr-Summers e5685ac806 ++RAC 2014-11-06 13:46:14 -08:00
Justin Spahr-Summers 7597115f53 Xcode lulz 2014-11-06 13:44:33 -08:00
Justin Spahr-Summers 8a4a52fb2c For “long delay” test, wait longer than the delay 2014-11-05 12:03:28 -08:00
Justin Spahr-Summers 775afe32c9 Move away from afterEach for expectations
The ordering between multiple afterEach blocks isn’t guaranteed, so
just run any expectations inline.
2014-11-05 12:01:42 -08:00
Justin Spahr-Summers c4d487ea41 Fix test bundle lookup
We can’t use self.class, because this is being defined in a category on
QuickSpec.
2014-11-05 11:53:17 -08:00
Justin Spahr-Summers 23d4e3e3ce Take ownership of the installed bundle itself too 2014-11-05 11:31:56 -08:00
Justin Spahr-Summers 3e6d0bf55c Fix compilation error in QuickSpec+SQRLFixtures 2014-11-04 16:10:19 -08:00
Justin Spahr-Summers 446ebb4501 ++RAC for Quick 2014-11-04 16:09:01 -08:00
Justin Spahr-Summers 656f742bee Migrate to new QuickConfiguration style 2014-11-04 15:45:02 -08:00
Justin Spahr-Summers f91f53c545 ++Quick 2014-11-04 15:18:23 -08:00
Justin Spahr-Summers 4a8004be83 Use undocumented withTimeout() instead of sleeping 2014-10-29 12:00:38 -07:00
Justin Spahr-Summers 970343b9ee Increase timeouts for test app termination 2014-10-28 16:04:13 -07:00
Justin Spahr-Summers 70dc382f8d Fix missing symbols, bundle ID failures, etc. 2014-10-28 15:56:43 -07:00
Justin Spahr-Summers c53a20d4de Finish upgrading to Nimble matcher syntax 2014-10-28 15:25:52 -07:00
Justin Spahr-Summers 3a462922f4 Refactor SQRLTestCase into a QuickSpec category 2014-10-28 13:35:45 -07:00
Justin Spahr-Summers 38840a492e Restore some imports I erroneously removed 2014-10-28 13:22:46 -07:00
Justin Spahr-Summers 70ace5c15b WIP migrate to Quick and Nimble spec formats 2014-10-28 13:10:27 -07:00
Justin Spahr-Summers 6ef1b62d44 Find libextobjc files in RAC for testing 2014-10-28 13:04:03 -07:00
Justin Spahr-Summers 3b9e8e6243 The Test config trolls again 2014-10-28 12:59:38 -07:00
Justin Spahr-Summers 21ffa5a45a Import Nimble and Quick errrrrywhere 2014-10-28 12:48:26 -07:00
Justin Spahr-Summers 55e4c5c42e Add Quick and Nimble to test target 2014-10-28 12:45:47 -07:00
Justin Spahr-Summers 38cd714fd2 Manually import/link everything ShipIt depends on
These have to be statically built into the ShipIt, since (by contrast)
dynamic frameworks could disappear out from under it during the update
process.
2014-10-28 12:42:22 -07:00
Justin Spahr-Summers dd69adc9ff Bump deployment target of tests 2014-10-28 12:30:50 -07:00
Justin Spahr-Summers cb07313ca7 Fix compilation of Squirrel source files 2014-10-28 12:30:44 -07:00
Justin Spahr-Summers ef930ca242 Update to jspahrsummers/objc-build-scripts@a54782 2014-10-28 12:29:02 -07:00
Justin Spahr-Summers f30b68945c Just generally fix a bunch of things in the project 2014-10-28 12:27:44 -07:00
Justin Spahr-Summers 31c0abff54 Fix RAC reference, 🔥 Specta and Expecta 2014-10-28 12:15:24 -07:00
Justin Spahr-Summers 4ca24da5ea ++MTL, ++RAC 2014-10-28 12:12:59 -07:00
Justin Spahr-Summers 43a7614322 Merge branch 'master' into xcode6 2014-10-28 12:12:30 -07:00
Keith Duncan 453b8697f9 Merge pull request #130 from Squirrel/timestamp-none
Don't use a timestamp for non-release builds
2014-09-06 06:55:36 +02:00
Matt Diephouse 693d513354 Add a timestamp argument instead 2014-09-05 14:57:45 -04:00
Matt Diephouse 9488a4cd12 Don't use a timestamp for release builds 2014-09-04 13:40:40 -04:00
Josh Abernathy a9ea214d11 Merge pull request #129 from Squirrel/resolve-top-level-symlinks
Resolve the top level symlink
2014-09-02 17:07:12 +02:00
Keith Duncan 6d91340ffa Resolve the top level symlink
When archiving uninstalled products component paths are a symlink to another dir.
2014-09-02 17:03:58 +02:00
Alan Rogers e8851b59bc Merge pull request #128 from robb/patch-1
Update Mantle link
2014-09-02 23:36:02 +10:00
Robert Böhnke 9539744417 Update Mantle link 2014-09-02 14:10:11 +02:00
Keith Duncan 19b011bee5 Merge pull request #127 from Squirrel/update-deep-codesign
🔥 Resource rules 🔥
2014-09-02 10:24:20 +02:00
joshaber 6ca74dc2c7 🔥 Resource rules 🔥 2014-09-02 10:20:47 +02:00
Justin Spahr-Summers cb2d4ecfd2 Merge pull request #126 from robb/patch-1
Fix anchor in README
2014-08-28 10:43:47 -07:00
Robert Böhnke 81c8b56e52 Fix anchor in README 2014-08-28 19:33:28 +02: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
Justin Spahr-Summers c245afac29 Don't automatically build unit tests 2014-07-16 11:39:57 -07:00
Justin Spahr-Summers 106e95b41f ++MTL and ++RAC 2014-07-16 11:39:56 -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
Justin Spahr-Summers ca100f1e09 ++RAC to v2.2.4 2014-02-17 11:23:07 -08: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
Josh Abernathy 36e0531218 Merge pull request #104 from Squirrel/dont-crash-with-no-signature
Log when we can’t get the running application code signature
2014-01-22 11:18:39 -08:00
Keith Duncan 77aec66456 Throw an exception when there’s no code signature in release 2014-01-22 11:08:31 -08:00
Keith Duncan e8523fd74d Log when we can’t get the running application code signature
Don’t crash.
2014-01-22 10:37:58 -08:00
Justin Spahr-Summers 767892c788 ++RAC to fix RACCommand KVO crashes 2014-01-21 14:36:05 -08:00
Rob Rix 34e37569d7 Merge pull request #103 from Squirrel/prune-old-updates
Prune temporary directories before performing first update check
2014-01-21 14:25:55 -08:00
Justin Spahr-Summers 4c93cb4508 Remove update directories before first check of any kind
Not just before beginning automatic updates.
2014-01-21 12:26:43 -08:00
Justin Spahr-Summers 431224c788 Test that updates are cleaned up 2014-01-21 12:16:35 -08:00
Justin Spahr-Summers ea8f805825 Remove old update directories before checking for updates 2014-01-21 11:55:52 -08:00
Justin Spahr-Summers 7bb4c131e1 Remove downloaded zips after unarchiving them 2014-01-21 11:43:32 -08:00
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
Justin Spahr-Summers 372a7d783a Merge pull request #101 from Squirrel/update-readme
Add steps to Readme
2014-01-20 15:57:27 -08:00
Justin Spahr-Summers b85e03fbe9 Minor ordering/phrasing tweaks 2014-01-20 15:56:31 -08:00
probablycorey 48455f1ee4 Add steps to get Squirrel to build 2014-01-20 15:47:46 -08:00
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
Justin Spahr-Summers 5cd5fdb2ed Merge pull request #97 from Squirrel/bridge-authorization
Don’t `__bridge` cast the `AuthorizationRef`
2014-01-07 10:49:48 -08:00
Keith Duncan aa545ed419 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	External/ReactiveCocoa
2014-01-07 18:06:35 +00:00
Keith Duncan 3d507a58a6 Check that _authorization is non NULL before freeing 2014-01-07 17:25:54 +00:00
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 406728c066 ++RAC 2014-01-06 12:09:29 -08:00
Keith Duncan 53adc17554 Types++ 2014-01-06 12:05:58 +00:00
Keith Duncan 0fb9ed48c9 Merge remote-tracking branch 'origin/master' into bridge-authorization
Conflicts:
	Squirrel/SQRLShipItLauncher.m
2014-01-06 12:04:39 +00:00
Keith Duncan 1db648059a Wrap the authorization in an Objective-C object for lifetime management 2014-01-06 12:01:12 +00:00
Keith Duncan fccf29122d Don’t __bridge cast the AuthorizationRef 2014-01-03 20:52:33 +00:00
Matt Diephouse 502c96a6df ++RAC 2013-12-30 14:04:02 -05:00
Josh Abernathy 90a6a2fe16 Merge pull request #94 from Squirrel/update-mantle-urls
Update URLs to Mantle repository
2013-12-13 14:57:10 -08:00
Justin Spahr-Summers c2869a05e9 Update URLs to Mantle repository
This change isn’t strictly necessary due to automatic redirection, but
let’s keep our links fresh.
2013-12-13 12:55:10 -08:00
Justin Spahr-Summers aa69586334 Merge pull request #90 from Squirrel/note-rac-dependency
Note that we're dependent on ReactiveCocoa and Mantle
2013-12-06 10:03:12 -08:00
Keith Duncan 8f3f5a5a72 Revert change to ReactiveCocoa pin 2013-12-06 12:53:55 +00:00
Keith Duncan f48ccd2a21 Float the version number dependency
Saves updating this when Squirrel switches to 3.0 too
2013-12-06 12:44:45 +00:00
Keith Duncan e21fd640b9 Commas-- 2013-12-05 14:14:01 +00:00
Keith Duncan 7bb7799b17 Change version dependency to 2.0 2013-12-05 14:13:33 +00:00
Keith Duncan 13a35d195b Clarify that the host application must embed the dependencies. 2013-12-04 12:28:13 +00:00
Keith Duncan 0c95e0c8c4 Note that we're dependent on ReactiveCocoa and Mantle
The client is responsible for including these because we don’t embed them.
2013-12-03 18:21:48 +00:00
Justin Spahr-Summers e5c65e530f Merge pull request #88 from Squirrel/skip-install
SKIP_INSTALL for ShipIt
2013-11-27 10:25:49 -08:00
Keith Duncan 65f191ce30 SKIP_INSTALL for ShipIt 2013-11-27 12:34:13 +00:00
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 2388fa3475 Merge pull request #83 from Squirrel/fix-test-races
Fix race conditions in tests
2013-11-19 11:27:42 -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 99bb3e042f Tighten up termination listening tests 2013-11-18 21:28:34 -08:00
Justin Spahr-Summers 5b930bcdb8 Don't log job removal errors in tests 2013-11-18 21:25:37 -08:00
Justin Spahr-Summers 7aace7204d Wait for a ShipIt state transition before sending signals 2013-11-18 21:21:44 -08:00
Justin Spahr-Summers c42950ccc4 Silence spurious "could not remove job" messages 2013-11-18 21:19:03 -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
Josh Abernathy efbb838e09 Merge pull request #82 from Squirrel/fix-codesigning
Unlock the keychain before building
2013-11-18 12:00:49 -08:00
Justin Spahr-Summers 249481c842 Unlock the keychain before building 2013-11-18 11:57:30 -08:00
Rob Rix 4062563d09 Merge pull request #81 from Squirrel/always-relaunch
Relaunch even if installation fails
2013-11-18 11:45:39 -08:00
Justin Spahr-Summers 55ca2cd87b ++RAC 2013-11-14 00:08:12 -08:00
Alan Rogers 6159fe07d5 Merge pull request #80 from Squirrel/should-relaunch-updating
Replace `shouldRelaunch` with a method to do it explicitly
2013-11-13 22:50:09 -08:00
Justin Spahr-Summers 2fed661399 indentation how does it work 2013-11-13 22:41:32 -08:00
Justin Spahr-Summers 1a9c430aca Merge pull request #79 from Squirrel/xctool
Use xctool for CI
2013-11-13 16:34:45 -08:00
Justin Spahr-Summers 2220774e01 Relaunch even if installation fails 2013-11-13 03:09:25 -08:00
Justin Spahr-Summers 77f5c72415 Failing test for relaunching after an update failure 2013-11-13 02:43:17 -08:00
Justin Spahr-Summers 3bd1e41270 Update README for RAC APIs 2013-11-13 02:00:35 -08:00
Justin Spahr-Summers 9b92542b9c Replay -relaunchToInstallUpdate 2013-11-13 01:57:51 -08:00
Justin Spahr-Summers 56b08482f4 Replace shouldRelaunch property with a method
This more closely aligns with the intention behind it anyways.
2013-11-13 01:38:39 -08:00
Justin Spahr-Summers c96467f35d Only build Squirrel scheme for CI 2013-11-12 23:12:50 -08:00
Justin Spahr-Summers 707678e926 Pipe TestApplication's output through NSLog too 2013-11-12 23:10:41 -08:00
Justin Spahr-Summers c50efc62a1 Capture output from tail and run it through NSLog
This avoids xctool dying on unexpected output.

See facebook/xctool#244.
2013-11-12 23:03:02 -08:00
Justin Spahr-Summers e4b7973685 Suppress deep-codesign output, fucks up xctool 2013-11-12 22:17:29 -08:00
Justin Spahr-Summers e4c1a33251 Stop logging killalls, fucks up xctool 2013-11-12 22:15:07 -08:00
Justin Spahr-Summers 7ba896fdde Revert "Disable deep codesigning"
This reverts commit 9fa097bec6.
2013-11-12 22:14:25 -08:00
Justin Spahr-Summers 9fa097bec6 Disable deep codesigning 2013-11-12 22:11:21 -08:00
Keith Duncan 9f8cbe17c3 Disable the deep codesign test
This is failing on 10.9 and is annoying
2013-11-12 22:08:53 -08:00
Justin Spahr-Summers 6f3e933d67 Update objc-build-scripts 2013-11-11 11:43:24 -08:00
Justin Spahr-Summers 516ab2af65 ++MTL and RAC to xctool branches 2013-11-11 11:43:15 -08:00
Josh Abernathy 4d385f4a33 Merge pull request #78 from Squirrel/fix-xcodebuild
Remove RunUnitTests script
2013-11-08 12:37:10 -08:00
Justin Spahr-Summers d5456f90f2 Remove RunUnitTests script
Not supported in Xcode 5's `xcodebuild`.
2013-11-08 12:35:07 -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
Justin Spahr-Summers 72d768d17d Merge pull request #74 from Squirrel/response-code-checking
Add response code checking to the check and download requests
2013-10-30 11:23:36 -07:00
Keith Duncan 91549567e1 Dot syntax++ 2013-10-30 11:45:58 +00:00
Keith Duncan 6e9efa0be3 :trollface: @jspahrsummers 2013-10-30 11:44:26 +00:00
Justin Spahr-Summers 8709338949 ++RAC to v2.1.6 2013-10-29 22:21:06 -07:00
Keith Duncan f9d6c00f60 Tweak comment 2013-10-29 11:27:39 +00:00
Keith Duncan e122927c5d Add test for non JSON data 2013-10-29 11:25:43 +00:00
Keith Duncan 43d524e186 Fix style 2013-10-29 11:15:49 +00:00
Keith Duncan a4dc3e4a47 Add test for non 2xx responses 2013-10-28 13:22:57 +00:00
Keith Duncan e5f838463b Make addCleanupBlock: public 2013-10-28 12:53:08 +00:00
Keith Duncan e24d538232 Add OHHTTPStubs 2013-10-28 12:53:01 +00:00
Keith Duncan 9170c4d106 Remove NSHTTPURLResponse cast, replace with id 2013-10-28 12:33:55 +00:00
Keith Duncan a93cf336b8 Use -reduceEach: returning signals then flatten
Replaces RACTupleUnpack
2013-10-28 12:33:23 +00:00
Keith Duncan 7bc6620b45 Split the updateURL class and content checking 2013-10-28 12:18:00 +00:00
Keith Duncan 4f0ba2c41a Add expects() for the written update
This was failing and expecting on the error would have given more details in the log
2013-10-25 14:54:06 +01:00
Keith Duncan cfaabf7cb0 Only check for nil hosts on non file:// scheme URLs
file scheme URLs have a nil host on 10.9
2013-10-25 14:53:58 +01:00
Keith Duncan d4cd9fa88c Add response code checking to the download request 2013-10-25 14:48:50 +01:00
Keith Duncan a7a3ddbf9c Add response code checking to the update request
Replace the -reduceEach: with a -flattenMap: which checks the response code, returns an empty signal if the response is No Content.
2013-10-25 14:44:16 +01:00
187 changed files with 10472 additions and 2741 deletions
+21 -5
View File
@@ -1,6 +1,22 @@
[submodule "External/ReactiveCocoa"]
path = External/ReactiveCocoa
[submodule "External/OHHTTPStubs"]
path = External/OHHTTPStubs
url = https://github.com/github/OHHTTPStubs
[submodule "Carthage.checkout/Nimble"]
path = Carthage/Checkouts/Nimble
url = https://github.com/Quick/Nimble.git
[submodule "Carthage.checkout/ReactiveCocoa"]
path = Carthage/Checkouts/ReactiveCocoa
url = https://github.com/ReactiveCocoa/ReactiveCocoa.git
[submodule "External/Mantle"]
path = External/Mantle
url = https://github.com/github/Mantle.git
[submodule "Carthage.checkout/Quick"]
path = Carthage/Checkouts/Quick
url = https://github.com/Quick/Quick.git
[submodule "Carthage.checkout/xcconfigs"]
path = Carthage/Checkouts/xcconfigs
url = https://github.com/jspahrsummers/xcconfigs.git
[submodule "Carthage.checkout/Mantle"]
path = Carthage/Checkouts/Mantle
url = https://github.com/Mantle/Mantle.git
[submodule "Carthage/Checkouts/Nimble"]
url = https://github.com/Quick/Nimble.git
[submodule "Carthage/Checkouts/Quick"]
url = https://github.com/Quick/Quick.git
+10
View File
@@ -0,0 +1,10 @@
osx_image: xcode8.3
language: objective-c
install: script/bootstrap
script: script/cibuild
notifications:
email: false
env:
global:
secure: L4C3hHzXc/sgL35dIz3kjqIzPWnXCHsFOgbOcz0f249zlnegeKHQdChm4HUDtfrbmKLMYQCIvVfcgPeLaHkCSeHqXUihXLT2LglK+0GX3MBbFzde9x1uln2O68hH1tTmBg2O2i4Dtc4D3e+2tVv3XmhQDZt0znIVO6n9YgevDqw=
+2
View File
@@ -0,0 +1,2 @@
github "Mantle/Mantle" ~> 1.5.3
github "ReactiveCocoa/ReactiveCocoa" ~> 2.4.3
+3
View File
@@ -0,0 +1,3 @@
github "jspahrsummers/xcconfigs" ~> 0.8.1
github "Quick/Quick" ~> 1.1.0
github "Quick/Nimble" ~> 6.1.0
+5
View File
@@ -0,0 +1,5 @@
github "Mantle/Mantle" "1.5.8"
github "Quick/Nimble" "v6.1.0"
github "Quick/Quick" "v1.1.0"
github "ReactiveCocoa/ReactiveCocoa" "v2.5"
github "jspahrsummers/xcconfigs" "0.8.1"
Vendored Submodule
+1
Vendored Submodule
+1
Vendored Submodule
+1
-1
Submodule External/Mantle deleted from 6cbdbebc44
Vendored Submodule
+1
Submodule External/OHHTTPStubs added at 08776e99bc
+120 -47
View File
@@ -6,6 +6,7 @@ and transparent as updates to a website**.
Instead of publishing a feed of versions from which your app must select,
Squirrel updates to the version your server tells it to. This allows you to
intelligently update your clients based on the request you give to Squirrel.
The server can remotely drive behaviors like rolling back or phased rollouts.
Your request can include authentication details, custom headers or a request
body so that your server has the context it needs in order to supply the most
@@ -13,21 +14,49 @@ 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)
# Adopting Squirrel
1. Install xctool with `brew install xctool`
1. Add the Squirrel repository as a git submodule
1. Add a reference to Squirrel.xcodeproj to your project
1. Run `script/bootstrap` from within the submodule
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 Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`) includes the
Frameworks directory Squirrel.framework is copied into
1. Ensure your application includes the [dependencies](#dependencies). Squirrel
does not embed them itself.
If you’re developing Squirrel on its own, then use `Squirrel.xcworkspace`.
# Dependencies
Squirrel depends on [ReactiveCocoa](http://github.com/ReactiveCocoa/ReactiveCocoa)
and [Mantle](https://github.com/Mantle/Mantle).
If your application is already using ReactiveCocoa, ensure it is using the same
version as Squirrel.
Otherwise, add a target dependency and Copy Files build phase entry for the
ReactiveCocoa.framework target included in Squirrel's repository, in
External/ReactiveCocoa.
Similarly, ensure your application includes Mantle, or copies in the Squirrel
version.
Finally, ensure your application's Runpath Search Paths (`LD_RUNPATH_SEARCH_PATHS`)
includes the directory that Squirrel.framework, ReactiveCocoa.framework
and Mantle.framework are copied into.
# Configuration
Once Squirrel is added to your project, you need to configure and start it.
@@ -35,44 +64,102 @@ 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"]]];
[self.updater startAutomaticChecksWithInterval:/* 4 Hours */ 60 * 60 * 4];
[self.updater checkForUpdates];
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];
}
```
Squirrel will periodically request and automatically download any updates.
Squirrel will periodically request and automatically download any updates. When
your application terminates, any downloaded update will be automatically
installed.
Before your application terminates, it should tell Squirrel to install any
updates that it has downloaded:
## Update 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`:
```objc
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
[self.updater installUpdateIfNeeded:^(BOOL success, NSError *error) {
if (success) {
NSLog(@"Successfully installed an update");
} else {
NSLog(@"Failed to update: %@", error);
}
[sender replyToApplicationShouldTerminate:YES];
}];
return NSTerminateLater;
}
[self.updater.updates subscribeNext:^(SQRLDownloadedUpdate *downloadedUpdate) {
NSLog(@"An update is ready to install: %@", downloadedUpdate);
}];
```
# Update JSON Format
## Installing Updates
Squirrel requests the URL you provide with `Accept: application/json` and
expects the following schema in response:
While downloaded updates are automatically installed when your application
terminates, if don't want to wait you can manually terminate the app to begin
the installation process immediately.
Once an [update available notification](#update-available-notifications) has
been received, you may want to present an interface informing the user about
the update and offering the ability to install and relaunch.
To explicitly install a downloaded update and automatically relaunch afterward,
subscribe to the `relaunchToInstallUpdate` signal on `SQRLUpdater`:
```objc
[[self.updater relaunchToInstallUpdate] subscribeError:^(NSError *error) {
NSLog(@"Error preparing update: %@", error);
}];
```
# Server Support
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
{
"url": "http://mycompany.com/myapp/releases/myrelease",
"name": "My Release Name",
"notes": "Theses are some release notes innit",
"pub_date": "2013-09-18T12:29:53+01:00",
"pub_date": "2013-09-18T12:29:53+01:00"
}
```
@@ -83,24 +170,10 @@ 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
Squirrel does not provide an updates interface, if you want to display available
updates, subscribe to the `SQRLUpdaterUpdateAvailableNotification` notification.
![:shipit:](http://shipitsquirrel.github.io/images/ship%20it%20squirrel.png)
Squirrel does not provide any GUI components for presenting updates. If you want
to indicate updates to the user, make sure to [listen for downloaded
updates](#update-available-notifications).
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
version = "1.3">
LastUpgradeVersion = "0830"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@@ -22,14 +22,14 @@
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.octest"
BuildableName = "SquirrelTests.xctest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
@@ -37,32 +37,61 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
shouldUseLaunchSchemeArgsEnv = "NO">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BEE179CC00E00158214"
BuildableName = "SquirrelTests.octest"
BuildableName = "SquirrelTests.xctest"
BlueprintName = "SquirrelTests"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "FORCE_APP_IDENTIFIER"
value = "com.github.Squirrel.SquirrelTests"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugXPCServices = "NO"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "SQRLUpdateFromURL"
@@ -74,11 +103,20 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Profile"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D0C22BD6179CC00E00158214"
BuildableName = "Squirrel.framework"
BlueprintName = "Squirrel"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
LastUpgradeVersion = "0830"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -23,10 +23,10 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
@@ -38,17 +38,21 @@
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable>
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -61,12 +65,13 @@
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
<BuildableProductRunnable>
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D08D4E0B17B451500012B22D"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0500"
LastUpgradeVersion = "0830"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -23,30 +23,42 @@
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Test"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
buildConfiguration = "Test">
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
buildConfiguration = "Debug"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D014AC0017B97885007D79D0"
BuildableName = "ShipIt"
BlueprintName = "ShipIt"
ReferencedContainer = "container:Squirrel.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
buildConfiguration = "Release"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
+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:Carthage/Checkouts/Mantle/Mantle.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/ReactiveCocoa/ReactiveCocoa.xcodeproj">
</FileRef>
<FileRef
location = "group:External/OHHTTPStubs/OHHTTPStubs/OHHTTPStubs.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Quick/Quick.xcodeproj">
</FileRef>
<FileRef
location = "group:Carthage/Checkouts/Nimble/Nimble.xcodeproj">
</FileRef>
</Workspace>
@@ -14,4 +14,7 @@
// Info.plist, or nil if the key is not present.
@property (nonatomic, copy, readonly) NSString *sqrl_bundleVersion;
/// The value of the `kCFBundleExecutableKey` key.
@property (nonatomic, copy, readonly) NSString *sqrl_executableName;
@end
@@ -14,4 +14,8 @@
return [self objectForInfoDictionaryKey:(id)kCFBundleVersionKey];
}
- (NSString *)sqrl_executableName {
return [self objectForInfoDictionaryKey:(id)kCFBundleExecutableKey];
}
@end
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/RACSignal.h>
@interface RACSignal (SQRLTransactionExtensions)
@@ -7,6 +7,8 @@
//
#import "RACSignal+SQRLTransactionExtensions.h"
#import <ReactiveCocoa/RACDisposable.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
// How long before power assertions time out.
+27
View File
@@ -0,0 +1,27 @@
//
// SQRLAuthorization.h
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
// Wraps an AuthorizationRef.
@interface SQRLAuthorization : NSObject
// Designated initializer.
//
// authorization - Must not be NULL, the returned object assumes ownership of
// the passed authorization.
//
// Returns an object which ties the `authorization` argument to the returned
// object's lifetime.
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization;
@property (readonly, nonatomic, assign) AuthorizationRef authorization;
@end
+28
View File
@@ -0,0 +1,28 @@
//
// SQRLAuthorization.m
// Squirrel
//
// Created by Keith Duncan on 06/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLAuthorization.h"
@implementation SQRLAuthorization
- (instancetype)initWithAuthorization:(AuthorizationRef)authorization {
NSParameterAssert(authorization != NULL);
self = [self init];
if (self == nil) return nil;
_authorization = authorization;
return self;
}
- (void)dealloc {
if (_authorization != NULL) AuthorizationFree(_authorization, kAuthorizationFlagDestroyRights);
}
@end
+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
+55 -35
View File
@@ -7,8 +7,11 @@
//
#import "SQRLCodeSignature.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "EXTKeyPathCoding.h"
#import "EXTScope.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <Security/Security.h>
NSString * const SQRLCodeSignatureErrorDomain = @"SQRLCodeSignatureErrorDomain";
@@ -19,9 +22,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 +45,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,35 +98,6 @@ 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 {
+16 -3
View File
@@ -6,13 +6,16 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
@class RACSignal;
// Provides the file locations that Squirrel/ShipIt use.
@interface SQRLDirectoryManager : NSObject
// The application identifier to use in file locations.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
// Returns the shared `SQRLDirectoryManager` for the running application, based
// on the bundle identifier or application name.
+ (instancetype)currentApplicationManager;
@@ -26,14 +29,24 @@
// on-disk locations for. This must not be nil.
- (instancetype)initWithApplicationIdentifier:(NSString *)appIdentifier;
// Finds or creates an Application Support folder.
// Finds or creates a storage folder.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)applicationSupportURL;
- (RACSignal *)storageURL;
// Determines where archived `SQRLShipItState` should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStateURL;
// Determines where ShipIt's stdout log should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStdoutURL;
// Determines where ShipIt's stderr log should be saved.
//
// Returns a signal which synchronously sends a URL then completes, or errors.
- (RACSignal *)shipItStderrURL;
@end
+24 -12
View File
@@ -7,14 +7,8 @@
//
#import "SQRLDirectoryManager.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface SQRLDirectoryManager ()
// The application identifier to use in file locations.
@property (nonatomic, copy, readonly) NSString *applicationIdentifier;
@end
#import <ReactiveCocoa/RACSignal+Operations.h>
@implementation SQRLDirectoryManager
@@ -30,7 +24,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.environment[@"FORCE_APP_IDENTIFIER"];
}
NSAssert(identifier != nil, @"Could not automatically determine the current application's identifier");
@@ -53,11 +47,11 @@
#pragma mark Folder URLs
- (RACSignal *)applicationSupportURL {
- (RACSignal *)storageURL {
return [[RACSignal
defer:^{
NSError *error = nil;
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
NSURL *folderURL = [NSFileManager.defaultManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:&error];
if (folderURL == nil) {
return [RACSignal error:error];
}
@@ -69,18 +63,36 @@
return [RACSignal return:folderURL];
}]
setNameWithFormat:@"%@ -applicationSupportURL", self];
setNameWithFormat:@"%@ -storageURL", self];
}
- (RACSignal *)shipItStateURL {
return [[[self
applicationSupportURL]
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipItState.plist"];
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
}
- (RACSignal *)shipItStdoutURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stdout.log"];
}]
setNameWithFormat:@"%@ -shipItStdoutURL", self];
}
- (RACSignal *)shipItStderrURL {
return [[[self
storageURL]
map:^(NSURL *folderURL) {
return [folderURL URLByAppendingPathComponent:@"ShipIt_stderr.log"];
}]
setNameWithFormat:@"%@ -shipItStderrURL", self];
}
#pragma mark NSObject
- (NSString *)description {
+2 -2
View File
@@ -40,9 +40,9 @@
#pragma mark MTLJSONSerializing
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return [super.JSONKeyPathsByPropertyKey mtl_dictionaryByAddingEntriesFromDictionary:@{
return @{
@keypath(SQRLDownloadedUpdate.new, bundleURL): NSNull.null
}];
};
}
@end
+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;
+17 -12
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,25 +34,34 @@ extern const NSInteger SQRLInstallerErrorInvalidState;
// There was an error moving a bundle across volumes.
extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
@class RACCommand;
@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
// When executed with a `SQRLShipItState`, attempts to install the update or
// Initializes an installer using the given application identifier, which is
// used to scope resumable state stored to user defaults.
//
// 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.
@property (nonatomic, strong, readonly) RACCommand *installUpdateCommand;
// When executed with a `SQRLShipItState`, 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 `installUpdateCommand` is executing.
//
@@ -60,8 +69,4 @@ extern const NSInteger SQRLInstallerErrorMovingAcrossVolumes;
// aborting/recovery has finished.
@property (nonatomic, strong, readonly) RACCommand *abortInstallationCommand;
// Initializes an installer using the given directory manager to read and write the
// state of the installation.
- (id)initWithDirectoryManager:(SQRLDirectoryManager *)directoryManager;
@end
+414 -371
View File
@@ -7,17 +7,24 @@
//
#import "SQRLInstaller.h"
#import <libkern/OSAtomic.h>
#import "EXTScope.h"
#import <ReactiveCocoa/NSEnumerator+RACSequenceAdditions.h>
#import <ReactiveCocoa/NSObject+RACPropertySubscribing.h>
#import <ReactiveCocoa/RACCommand.h>
#import <ReactiveCocoa/RACSequence.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
#import <sys/xattr.h>
#import "NSBundle+SQRLVersionExtensions.h"
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLShipItState.h"
#import "SQRLShipItRequest.h"
#import "SQRLTerminationListener.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 +35,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 +98,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,19 +112,28 @@ 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);
@@ -156,42 +145,63 @@ typedef struct {
switchToLatest]
setNameWithFormat:@"aborting"];
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItState *state) {
_installUpdateCommand = [[RACCommand alloc] initWithEnabled:[aborting not] signalBlock:^(SQRLShipItRequest *request) {
@strongify(self);
NSParameterAssert(state != nil);
NSParameterAssert(request != 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];
}];
// 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.
_abortInstallationCommand = [[RACCommand alloc] initWithEnabled:[self.installUpdateCommand.executing not] signalBlock:^(SQRLShipItState *state) {
@strongify(self);
NSParameterAssert(state != nil);
return [[[RACSignal
zip:@[
[self getRequiredKey:@keypath(state.targetBundleURL) fromState:state],
[self getRequiredKey:@keypath(state.codeSignature) fromState:state]
] reduce:^(NSURL *targetBundleURL, SQRLCodeSignature *codeSignature) {
return [self verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:state.backupBundleURL];
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);
}]
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];
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 Installer State
#pragma mark Preferences
- (RACSignal *)getRequiredKey:(NSString *)key fromState:(SQRLShipItState *)state {
- (SQRLInstallerOwnedBundle *)ownedBundle {
id archiveData = CFBridgingRelease(CFPreferencesCopyValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFStringRef)self.applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost));
if (![archiveData isKindOfClass:NSData.class]) return nil;
SQRLInstallerOwnedBundle *ownedBundle = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
if (![ownedBundle isKindOfClass:SQRLInstallerOwnedBundle.class]) return nil;
return ownedBundle;
}
- (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 Properties
- (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]];
@@ -199,271 +209,184 @@ 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:) },
{ .installerState = SQRLInstallerStateRelaunching, .selector = @selector(relaunchWithState:) },
};
- (RACSignal *)prepareAndValidateUpdateBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
const size_t tableCount = sizeof(dispatchTablePrototype) / sizeof(*dispatchTablePrototype);
NSValue *boxedDispatchTable = [NSValue valueWithBytes:dispatchTablePrototype objCType:@encode(__typeof__(dispatchTablePrototype))];
return [[[[[[[self
ownedTemporaryDirectoryURL]
flattenMap:^(NSURL *directoryURL) {
return [self copyBundleAtURL:request.updateBundleURL toDirectory:directoryURL];
}]
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 *step = [RACSignal defer:^{
SQRLInstallerDispatchTableEntry dispatchTable[tableCount];
[boxedDispatchTable getValue:&dispatchTable];
- (RACSignal *)acquireTargetBundleURLForRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
SQRLInstallerState installerState = state.installerState;
return [[[[RACSignal
zip:@[
[self ownedTemporaryDirectoryURL],
[self codeSignatureForBundleAtURL:request.targetBundleURL],
] reduce:^(NSURL *directoryURL, SQRLCodeSignature *codeSignature) {
NSURL *targetBundleURL = request.targetBundleURL;
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:targetBundleURL.lastPathComponent];
size_t tableIndex;
for (tableIndex = 0; tableIndex < tableCount; tableIndex++) {
if (dispatchTable[tableIndex].installerState == installerState) {
break;
}
}
return [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:request.targetBundleURL temporaryURL:newBundleURL codeSignature:codeSignature];
}]
doNext:^(SQRLInstallerOwnedBundle *ownedBundle) {
self.ownedBundle = ownedBundle;
}]
flattenMap:^(SQRLInstallerOwnedBundle *ownedBundle) {
return [self installItemToURL:ownedBundle.temporaryURL fromURL:ownedBundle.originalURL];
}]
setNameWithFormat:@"%@ -acquireTargetBundleURLForRequest: %@", self, request];
}
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);
}]
then:^{
return [RACSignal return:@(nextState)];
}];
}];
- (RACSignal *)renameIfNeeded:(SQRLShipItRequest *)request updateBundleURL:(NSURL *)updateBundleURL {
if (!request.useUpdateBundleName) return [RACSignal return:request];
return [[self
stepRepeatedly:step withState:state]
setNameWithFormat:@"%@ -resumeInstallationFromState: %@", self, state];
}
renamedTargetIfNeededWithTargetURL:request.targetBundleURL sourceURL:updateBundleURL]
flattenMap:^(NSURL *newTargetURL) {
if ([newTargetURL isEqual:request.targetBundleURL]) return [RACSignal return:request];
- (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];
}]
setNameWithFormat:@"%@ -clearQuarantineWithState: %@", self, state];
}
- (RACSignal *)backUpWithState:(SQRLShipItState *)state {
NSParameterAssert(state != 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];
}
return [skipBackup flattenMap:^(NSNumber *skip) {
if (skip.boolValue) {
return [RACSignal empty];
} else {
return [self backUpBundleAtURL:bundleURL];
}
}];
}]
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];
}]
setNameWithFormat:@"%@ -backUpWithState: %@", self, state];
}
- (RACSignal *)installWithState:(SQRLShipItState *)state {
NSParameterAssert(state != 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];
}
}]
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]
then:^{
// Recovery succeeded, but we still want to pass
// through the original error.
return [RACSignal error:error];
}];
}];
}]
flatten]
setNameWithFormat:@"%@ -installWithState: %@", self, state];
}
- (RACSignal *)verifyInPlaceWithState:(SQRLShipItState *)state {
NSParameterAssert(state != nil);
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) {
SQRLShipItRequest *updatedRequest = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:newTargetURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:request.launchAfterInstallation useUpdateBundleName:request.useUpdateBundleName];
return [[self
verifyBundleAtURL:targetBundleURL usingSignature:codeSignature recoveringUsingBackupAtURL:backupBundleURL]
then:^{
return [[self
deleteBackupAtURL:backupBundleURL]
catchTo:[RACSignal empty]];
installItemToURL:newTargetURL fromURL:request.targetBundleURL]
concat:[RACSignal return:updatedRequest]];
}];
}
- (RACSignal *)installRequest:(SQRLShipItRequest *)request {
NSParameterAssert(request != nil);
return [[[[self
prepareAndValidateUpdateBundleURLForRequest:request]
flattenMap:^(NSURL *updateBundleURL) {
return [[[[self
renameIfNeeded:request updateBundleURL:updateBundleURL]
flattenMap:^(SQRLShipItRequest *request) {
return [[self acquireTargetBundleURLForRequest:request] concat:[RACSignal return:request]];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [[[[[[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]];
}]
concat:[RACSignal return:request]];
}]
doCompleted:^{
self.ownedBundle = nil;
}];
}]
flatten]
setNameWithFormat:@"%@ -verifyInPlaceWithState: %@", 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 *)relaunchWithState:(SQRLShipItState *)state {
return [[[[RACSignal
- (RACSignal *)abortInstall {
// The request may have been tampered with to select a new targetURL to
// which the moved bundles should be restored.
//
// Discard the request parameters and restore the owned bundle to its
// original location.
SQRLInstallerOwnedBundle *ownedBundle = self.ownedBundle;
if (ownedBundle == nil) return [RACSignal empty];
return [[[[self
installItemToURL:ownedBundle.originalURL fromURL:ownedBundle.temporaryURL]
doCompleted:^{
self.ownedBundle = nil;
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Aborting update", nil) description:NSLocalizedString(@"An update to %@ is being rolled back, and interrupting the process could corrupt the application", nil), ownedBundle.originalURL.path]
setNameWithFormat:@"%@ -abortInstall", self];
}
#pragma mark Bundle Ownership
- (RACSignal *)ownedTemporaryDirectoryURL {
return [[[RACSignal
defer:^{
if (state.relaunchAfterInstallation) {
return [self getRequiredKey:@keypath(state.targetBundleURL) fromState:state];
} else {
return [RACSignal empty];
}
}]
deliverOn:RACScheduler.mainThreadScheduler]
flattenMap:^(NSURL *bundleURL) {
NSError *error = nil;
if ([NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:nil error:&error]) {
return [RACSignal empty];
} else {
return [RACSignal error:error];
}
}]
setNameWithFormat:@"%@ -relaunch", self];
}
NSString *tmpPath = [NSTemporaryDirectory() stringByResolvingSymlinksInPath];
NSString *template = [NSString stringWithFormat:@"%@.XXXXXXXX", self.applicationIdentifier];
#pragma mark Backing Up
char *fullTemplate = strdup([tmpPath stringByAppendingPathComponent:template].UTF8String);
@onExit {
free(fullTemplate);
};
- (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];
if (mkdtemp(fullTemplate) == NULL) {
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:nil]];
}
return [RACSignal return:temporaryDirectoryURL];
NSURL *URL = [NSURL fileURLWithPath:[NSFileManager.defaultManager stringWithFileSystemRepresentation:fullTemplate length:strlen(fullTemplate)] isDirectory:YES];
return [RACSignal return:URL];
}]
catch:^(NSError *error) {
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create backup folder", nil)];
NSString *description = [NSString stringWithFormat:NSLocalizedString(@"Could not create temporary 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];
setNameWithFormat:@"%@ -ownedDirectoryURL", self];
}
- (RACSignal *)deleteBackupAtURL:(NSURL *)backupURL {
NSParameterAssert(backupURL != nil);
- (RACSignal *)copyBundleAtURL:(NSURL *)bundleURL toDirectory:(NSURL *)directoryURL {
NSParameterAssert(bundleURL != nil);
NSParameterAssert(directoryURL != nil);
NSURL *newBundleURL = [directoryURL URLByAppendingPathComponent:bundleURL.lastPathComponent];
return [[[RACSignal
defer:^{
NSError *error = nil;
if ([NSFileManager.defaultManager removeItemAtURL:backupURL error:&error]) {
NSError *error;
BOOL copy = [NSFileManager.defaultManager copyItemAtURL:bundleURL toURL:newBundleURL error:&error];
if (!copy) return [RACSignal error:error];
return [RACSignal return:newBundleURL];
}]
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];
}
- (RACSignal *)deleteOwnedBundleAtURL:(NSURL *)bundleURL {
NSParameterAssert(bundleURL != nil);
return [[[RACSignal
defer:^{
NSError *error;
if ([NSFileManager.defaultManager removeItemAtURL:bundleURL error:&error]) {
return [RACSignal empty];
} else {
return [RACSignal error:error];
@@ -471,68 +394,90 @@ typedef struct {
}]
then:^{
// Also remove the temporary directory that the backup lived in.
NSURL *temporaryDirectoryURL = backupURL.URLByDeletingLastPathComponent;
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]];
int code = errno;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) {
userInfo[NSLocalizedDescriptionKey] = @(desc);
} else {
userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Unknown POSIX error", @"");
}
userInfo[NSLocalizedFailureReasonErrorKey] = [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove temp dir \"%@\"", @""), temporaryDirectoryURL.path];
userInfo[NSURLErrorKey] = temporaryDirectoryURL;
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]];
}
}]
setNameWithFormat:@"%@ -deleteBackupAtURL: %@", self, backupURL];
setNameWithFormat:@"%@ -deleteOwnedBundleAtURL: %@", self, bundleURL];
}
- (RACSignal *)verifyBundleAtURL:(NSURL *)bundleURL usingSignature:(SQRLCodeSignature *)signature recoveringUsingBackupAtURL:(NSURL *)backupBundleURL {
#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 [[[signature
verifyBundleAtURL:bundleURL]
catch:^(NSError *error) {
if (backupBundleURL == nil) return [RACSignal error:error];
return [[[[self
installItemAtURL:bundleURL fromURL:backupBundleURL]
initially:^{
[NSFileManager.defaultManager removeItemAtURL:bundleURL error:NULL];
}]
doCompleted:^{
NSLog(@"Restored backup bundle to %@", bundleURL);
}]
doError:^(NSError *recoveryError) {
NSLog(@"Could not restore backup bundle %@ to %@: %@", backupBundleURL, bundleURL, recoveryError.sqrl_verboseDescription);
}];
return [[[self
takeOwnershipOfDirectory:bundleURL]
then:^{
return [signature verifyBundleAtURL:bundleURL];
}]
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@ recoveringUsingBackupAtURL: %@", self, bundleURL, signature, backupBundleURL];
setNameWithFormat:@"%@ -verifyBundleAtURL: %@ usingSignature: %@", self, bundleURL, signature];
}
#pragma mark Installation
- (RACSignal *)checkWhetherBundlePreviouslyAtURL:(NSURL *)sourceURL wasInstalledAtURL:(NSURL *)targetURL usingSignature:(SQRLCodeSignature *)signature {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
NSParameterAssert(signature != nil);
/// Check if the target should be renamed and provide the renamed URL.
///
/// targetURL - The URL for the target. Cannot be nil.
/// sourceURL - The URL for the source. Cannot be nil.
///
/// Returns a signal which will send the URL for the renamed target. If a rename
/// isn't needed then it will send `targetURL`.
- (RACSignal *)renamedTargetIfNeededWithTargetURL:(NSURL *)targetURL sourceURL:(NSURL *)sourceURL {
return [RACSignal defer:^{
NSBundle *sourceBundle = [NSBundle bundleWithURL:sourceURL];
NSString *targetExecutableName = targetURL.lastPathComponent.stringByDeletingPathExtension;
NSString *sourceExecutableName = sourceBundle.sqrl_executableName;
return [[[[self
verifyBundleAtURL:targetURL usingSignature:signature recoveringUsingBackupAtURL:nil]
then:^{
return [RACSignal return:@YES];
}]
catch:^(NSError *error) {
BOOL directory;
if ([NSFileManager.defaultManager fileExistsAtPath:sourceURL.path isDirectory:&directory]) {
// If the source still exists, this isn't an error.
return [RACSignal return:@NO];
} else {
return [RACSignal error:error];
}
}]
setNameWithFormat:@"%@ -checkWhetherBundlePreviouslyAtURL: %@ wasInstalledAtURL: %@", self, sourceURL, targetURL];
// If they're already the same then we're good.
if ([targetExecutableName isEqual:sourceExecutableName]) {
return [RACSignal return:targetURL];
}
NSString *newAppName = [sourceExecutableName stringByAppendingPathExtension:@"app"];
NSURL *newTargetURL = [targetURL.URLByDeletingLastPathComponent URLByAppendingPathComponent:newAppName];
// If there's already something there then don't rename to it.
if ([NSFileManager.defaultManager fileExistsAtPath:newTargetURL.path]) {
return [RACSignal return:targetURL];
}
return [RACSignal return:newTargetURL];
}];
}
- (RACSignal *)installItemAtURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
- (RACSignal *)installItemToURL:(NSURL *)targetURL fromURL:(NSURL *)sourceURL {
NSParameterAssert(targetURL != nil);
NSParameterAssert(sourceURL != nil);
@@ -544,7 +489,7 @@ typedef struct {
} else {
int code = errno;
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
@@ -596,9 +541,16 @@ typedef struct {
// file to begin with.
if (code != ENOATTR) {
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
const char *desc = strerror(code);
if (desc != NULL) userInfo[NSLocalizedDescriptionKey] = @(desc);
if (desc != NULL) {
userInfo[NSLocalizedDescriptionKey] = @(desc);
} else {
userInfo[NSLocalizedDescriptionKey] = NSLocalizedString(@"Unknown POSIX error", @"");
}
userInfo[NSLocalizedFailureReasonErrorKey] = [NSString stringWithFormat:NSLocalizedString(@"Couldn't remove quarantine attribute from \"%@\". This most likely means the file is read-only.", @""), URL.path];
userInfo[NSURLErrorKey] = URL;
return [RACSignal error:[NSError errorWithDomain:NSPOSIXErrorDomain code:code userInfo:userInfo]];
}
@@ -609,6 +561,97 @@ 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
startWith:directoryURL]
subscribe:subscriber];
}]
flattenMap:^(NSURL *itemURL) {
return [[[self
readFileSecurityOfURL:itemURL]
flattenMap:^(NSFileSecurity *fileSecurity) {
if (![self takeOwnershipOfFileSecurity:fileSecurity]) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Permissions Error", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Couldn’t update permissions of %@", nil), itemURL.path],
NSURLErrorKey: itemURL
};
return [RACSignal error:[NSError errorWithDomain:SQRLInstallerErrorDomain code:SQRLInstallerErrorChangingPermissions userInfo:errorInfo]];
}
return [RACSignal return:fileSecurity];
}]
flattenMap:^(NSFileSecurity *fileSecurity) {
return [self writeFileSecurity:fileSecurity toURL:itemURL];
}];
}]
setNameWithFormat:@"%@ -takeOwnershipOfDirectory: %@", self, directoryURL];
}
- (BOOL)takeOwnershipOfFileSecurity:(NSFileSecurity *)fileSecurity {
CFFileSecurityRef actualFileSecurity = (__bridge CFFileSecurityRef)fileSecurity;
// If ShipIt is running as root, this will change the owner to
// root:wheel.
if (!CFFileSecuritySetOwner(actualFileSecurity, getuid())) return NO;
if (!CFFileSecuritySetGroup(actualFileSecurity, getgid())) return NO;
mode_t fileMode = 0;
if (!CFFileSecurityGetMode(actualFileSecurity, &fileMode)) return NO;
// Remove write permission from group and other, leave executable
// bit as it was for both.
//
// Permissions will be r-(x?)r-(x?) afterwards, with owner
// permissions left as is.
fileMode = (fileMode & ~(S_IWGRP | S_IWOTH));
return CFFileSecuritySetMode(actualFileSecurity, fileMode);
}
#pragma mark Error Handling
- (NSError *)missingDataErrorWithDescription:(NSString *)description {
+37
View File
@@ -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 "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
+30 -25
View File
@@ -13,6 +13,7 @@
#import <Security/Security.h>
#import <ServiceManagement/ServiceManagement.h>
#import <launch.h>
#import "SQRLAuthorization.h"
NSString * const SQRLShipItLauncherErrorDomain = @"SQRLShipItLauncherErrorDomain";
@@ -21,8 +22,7 @@ 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"];
return [SQRLDirectoryManager.currentApplicationManager.applicationIdentifier stringByAppendingString:@".ShipIt"];
}
+ (RACSignal *)shipItJobDictionary {
@@ -31,9 +31,13 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:jobLabel];
return [directoryManager applicationSupportURL];
return [RACSignal zip:@[
[directoryManager shipItStdoutURL],
[directoryManager shipItStderrURL],
[directoryManager shipItStateURL]
]];
}]
map:^(NSURL *appSupportURL) {
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL, NSURL *shipItStateURL) {
NSBundle *squirrelBundle = [NSBundle bundleForClass:self.class];
NSAssert(squirrelBundle != nil, @"Could not open Squirrel.framework bundle");
@@ -56,14 +60,16 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
// Pass in the service name so ShipIt knows how to broadcast itself.
[arguments addObject:jobLabel];
// We need to pass the path to ShipIt rather than having ShipIt
// calculate the path itself. This is because ShipIt may run as a
// different user if it had to escalate to be able to replace the
// bundle.
[arguments addObject:shipItStateURL.path];
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;
jobDict[@(LAUNCH_JOBKEY_STANDARDOUTPATH)] = stdoutURL.path;
jobDict[@(LAUNCH_JOBKEY_STANDARDERRORPATH)] = stderrURL.path;
#if DEBUG
jobDict[@(LAUNCH_JOBKEY_DEBUG)] = @YES;
#endif
return jobDict;
}]
setNameWithFormat:@"+shipItJobDictionary"];
@@ -71,7 +77,7 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
+ (RACSignal *)shipItAuthorization {
return [[RACSignal
createSignal:^(id<RACSubscriber> subscriber) {
createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
AuthorizationItem rightItems[] = {
{
.name = kSMRightModifySystemDaemons,
@@ -108,16 +114,15 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
AuthorizationRef authorization = NULL;
OSStatus authorizationError = AuthorizationCreate(&rights, &environment, kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights, &authorization);
if (authorizationError == noErr) {
[subscriber sendNext:(__bridge id)authorization];
[subscriber sendNext:[[SQRLAuthorization alloc] initWithAuthorization:authorization]];
[subscriber sendCompleted];
} else {
[subscriber sendError:[NSError errorWithDomain:NSOSStatusErrorDomain code:authorizationError userInfo:nil]];
}
return [RACDisposable disposableWithBlock:^{
if (authorization != NULL) AuthorizationFree(authorization, kAuthorizationFlagDestroyRights);
}];
return nil;
}]
setNameWithFormat:@"+shipItAuthorization"];
}
@@ -127,22 +132,22 @@ const NSInteger SQRLShipItLauncherErrorCouldNotStartService = 1;
zip:@[
self.shipItJobDictionary,
(privileged ? self.shipItAuthorization : [RACSignal return:nil])
] reduce:^(NSDictionary *jobDictionary, id authorization) {
] reduce:^(NSDictionary *jobDictionary, SQRLAuthorization *authorizationValue) {
CFStringRef domain = (privileged ? kSMDomainSystemLaunchd : kSMDomainUserLaunchd);
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, (__bridge AuthorizationRef)authorization, true, &cfError)) {
#if DEBUG
NSLog(@"Could not remove previous ShipIt job: %@", cfError);
#endif
AuthorizationRef authorization = authorizationValue.authorization;
if (cfError != NULL) {
CFRelease(cfError);
cfError = NULL;
CFErrorRef cfError;
if (!SMJobRemove(domain, (__bridge CFStringRef)self.shipItJobLabel, authorization, true, &cfError)) {
NSError *error = CFBridgingRelease(cfError);
cfError = NULL;
if (![error.domain isEqual:(__bridge id)kSMErrorDomainLaunchd] || error.code != kSMErrorJobNotFound) {
NSLog(@"Could not remove previous ShipIt job: %@", error);
}
}
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, (__bridge AuthorizationRef)authorization, &cfError)) {
if (!SMJobSubmit(domain, (__bridge CFDictionaryRef)jobDictionary, authorization, &cfError)) {
return [RACSignal error:CFBridgingRelease(cfError)];
}
+100
View File
@@ -0,0 +1,100 @@
//
// SQRLShipItRequest.h
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import <Mantle/Mantle.h>
@class RACSignal;
// The domain for errors originating within `SQRLShipItRequest`.
extern NSString * const SQRLShipItRequestErrorDomain;
// Errors originating from the `SQRLShipItRequestErrorDomain`.
//
// SQRLShipItRequestErrorMissingRequiredProperty - A required property was `nil`
// upon initialization.
//
// The `userInfo` dictionary for
// this error will contain
// `SQRLShipItStatePropertyErrorKey`.
//
// SQRLShipItRequestErrorUnarchiving - The saved request could not
// be unarchived, possibly
// because it's invalid.
//
// SQRLShipItRequestErrorArchiving - The request object could not
// be archived.
typedef enum : NSInteger {
SQRLShipItRequestErrorMissingRequiredProperty = 1,
SQRLShipItRequestErrorUnarchiving = 2,
SQRLShipItRequestErrorArchiving = 3,
} SQRLShipItRequestError;
// Associated with an `NSString` indicating the required property key that did
// not have a value upon initialization.
extern NSString * const SQRLShipItRequestPropertyErrorKey;
// Constructed and written to disk for `ShipIt` to pick up. This represents a
// single update request from the client's perspective.
@interface SQRLShipItRequest : MTLModel
// Reads a `SQRLShipItState` from disk, at the location specified by the given
// URL signal.
//
// URLSignal - Determines the file location to read from, the signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously send a `SQRLShipItRequest` then
// complete, or error.
+ (RACSignal *)readUsingURL:(RACSignal *)URLSignal;
// Writes the receiver to disk, at the location specified by the given URL
// signal.
//
// URLSignal - Determines the file location to write to. The signal should send
// an `NSURL` object then complete, or error. This must not be nil.
//
// Returns a signal which will synchronously complete or error.
- (RACSignal *)writeUsingURL:(RACSignal *)URLSignal;
// Designated initialiser.
//
// updateBundleURL - The update bundle which will replace
// targetBundleURL. Must not be nil.
// targetBundleURL - Where the update should be installed, if a bundle
// is already present, the update is checked for
// suitability against this bundle. Must not be nil.
// bundleIdentifier - The bundle identifier that the installer should
// wait for instances of to terminate before
// installing. Can be nil.
// launchAfterInstallation - Whether the updated application should be launched
// after installation.
// useUpdateBundleName - Should the target use the update bundle's name?
//
// 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 useUpdateBundleName:(BOOL)useUpdateBundleName;
// 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;
// Whether the app should use the update bundle's name.
@property (nonatomic, assign, readonly) BOOL useUpdateBundleName;
@end
+194
View File
@@ -0,0 +1,194 @@
//
// SQRLShipItRequest.m
// Squirrel
//
// Created by Keith Duncan on 08/01/2014.
// Copyright (c) 2014 GitHub. All rights reserved.
//
#import "SQRLShipItRequest.h"
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/RACSignal+Operations.h>
NSString * const SQRLShipItRequestErrorDomain = @"SQRLShipItRequestErrorDomain";
NSString * const SQRLShipItRequestPropertyErrorKey = @"SQRLShipItRequestPropertyErrorKey";
@interface SQRLShipItRequest () <MTLJSONSerializing>
@end
@implementation SQRLShipItRequest
#pragma mark Lifecycle
- (id)initWithDictionary:(NSDictionary *)dictionary error:(NSError **)error {
self = [super initWithDictionary:dictionary error:error];
if (self == nil) return nil;
BOOL (^validateKey)(NSString *) = ^(NSString *key) {
if ([self valueForKey:key] != nil) return YES;
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Missing required value", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"\"%@\" must not be set to nil.", nil), key]
};
*error = [NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorMissingRequiredProperty userInfo:userInfo];
}
return NO;
};
if (!validateKey(@keypath(self.targetBundleURL))) return nil;
if (!validateKey(@keypath(self.updateBundleURL))) return nil;
return self;
}
- (instancetype)initWithUpdateBundleURL:(NSURL *)updateBundleURL targetBundleURL:(NSURL *)targetBundleURL bundleIdentifier:(NSString *)bundleIdentifier launchAfterInstallation:(BOOL)launchAfterInstallation useUpdateBundleName:(BOOL)useUpdateBundleName {
return [self initWithDictionary:@{
@keypath(self.updateBundleURL): updateBundleURL,
@keypath(self.targetBundleURL): targetBundleURL,
@keypath(self.bundleIdentifier): bundleIdentifier ?: NSNull.null,
@keypath(self.launchAfterInstallation): @(launchAfterInstallation),
@keypath(self.useUpdateBundleName): @(useUpdateBundleName),
} error:NULL];
}
#pragma mark Serialization
+ (NSDictionary *)JSONKeyPathsByPropertyKey {
return @{};
}
+ (NSValueTransformer *)updateBundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (NSValueTransformer *)targetBundleURLJSONTransformer {
return [NSValueTransformer valueTransformerForName:MTLURLValueTransformerName];
}
+ (RACSignal *)readUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[[URL
flattenMap:^(NSURL *stateURL) {
NSError *error;
NSData *data = [self readFromURL:stateURL error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
flattenMap:^(NSData *data) {
return [self readFromData:data];
}]
catch:^(NSError *error) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not read update request", nil),
};
if (error != nil) {
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
NSUnderlyingErrorKey: error,
}];
}
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorUnarchiving userInfo:userInfo]];
}]
setNameWithFormat:@"+readUsingURL: %@", URL];
}
+ (NSData *)readFromURL:(NSURL *)URL error:(NSError **)errorRef {
__block NSData *data = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateReadingItemAtURL:URL options:NSFileCoordinatorReadingWithoutChanges error:errorRef byAccessor:^(NSURL *newURL) {
data = [NSData dataWithContentsOfURL:newURL options:NSDataReadingUncached error:errorRef];
}];
return data;
}
+ (RACSignal *)readFromData:(NSData *)data {
return [[RACSignal
defer:^{
NSError *error;
NSDictionary *JSONDictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (JSONDictionary == nil) {
return [RACSignal error:error];
}
SQRLShipItRequest *request = [MTLJSONAdapter modelOfClass:SQRLShipItRequest.class fromJSONDictionary:JSONDictionary error:&error];
if (request == nil) {
return [RACSignal error:error];
}
return [RACSignal return:request];
}]
setNameWithFormat:@"+readFromData: <NSData %p>", data];
}
- (RACSignal *)writeUsingURL:(RACSignal *)URL {
NSParameterAssert(URL != nil);
return [[[[RACSignal
zip:@[
URL,
[self serialization]
] reduce:^(NSURL *stateURL, NSData *data) {
NSError *error;
BOOL write = [self writeData:data toURL:stateURL error:&error];
if (!write) {
return [RACSignal error:error];
}
return [RACSignal empty];
}]
flatten]
catch:^(NSError *error) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Could not write update request", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"An unknown error occurred while archiving.", nil),
};
if (error != nil) {
userInfo = [userInfo mtl_dictionaryByAddingEntriesFromDictionary:@{
NSUnderlyingErrorKey: error,
}];
}
return [RACSignal error:[NSError errorWithDomain:SQRLShipItRequestErrorDomain code:SQRLShipItRequestErrorArchiving userInfo:userInfo]];
}]
setNameWithFormat:@"%@ -writeUsingURL: %@", self, URL];
}
- (BOOL)writeData:(NSData *)data toURL:(NSURL *)URL error:(NSError **)errorRef {
__block BOOL success = NO;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[coordinator coordinateWritingItemAtURL:URL options:0 error:errorRef byAccessor:^(NSURL *newURL) {
success = [data writeToURL:newURL options:NSDataWritingAtomic error:errorRef];
}];
return success;
}
- (RACSignal *)serialization {
return [[RACSignal
defer:^{
NSDictionary *JSONDictionary = [MTLJSONAdapter JSONDictionaryFromModel:self];
NSError *error;
NSData *data = [NSJSONSerialization dataWithJSONObject:JSONDictionary options:0 error:&error];
if (data == nil) {
return [RACSignal error:error];
}
return [RACSignal return:data];
}]
setNameWithFormat:@"%@ serialization", self];
}
@end
-121
View File
@@ -1,121 +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.
// SQRLInstallerStateRelaunching - Relaunching the updated application.
// This state will be entered even if
// there's no relaunching to do.
//
// Note that these values must remain backwards compatible, so ShipIt doesn't
// start up in a weird mode on a newer version.
typedef enum : NSInteger {
SQRLInstallerStateNothingToDo = 0,
SQRLInstallerStateClearingQuarantine,
SQRLInstallerStateBackingUp,
SQRLInstallerStateInstalling,
SQRLInstallerStateVerifyingInPlace,
SQRLInstallerStateRelaunching
} 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
+1 -1
View File
@@ -6,7 +6,7 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
@class RACSignal;
+8 -2
View File
@@ -7,7 +7,14 @@
//
#import "SQRLTerminationListener.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "EXTKeyPathCoding.h"
#import <ReactiveCocoa/NSArray+RACSequenceAdditions.h>
#import <ReactiveCocoa/RACDisposable.h>
#import <ReactiveCocoa/RACScheduler.h>
#import <ReactiveCocoa/RACSequence.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACSubscriber.h>
@interface SQRLTerminationListener ()
@@ -74,7 +81,6 @@
dispatch_resume(source);
return [RACDisposable disposableWithBlock:^{
dispatch_source_cancel(source);
dispatch_release(source);
}];
}]
setNameWithFormat:@"%@ -waitForTerminationOfProcessIdentifier: %i", self, (int)processIdentifier];
+17 -3
View File
@@ -74,7 +74,7 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
+ (NSValueTransformer *)releaseDateJSONTransformer {
// ISO 8601 Time Zone with ':'
NSString * const ISO8601DateFormat = @"yyyy'-'MM'-'DD'T'HH':'mm':'ssZZZZZ";
NSString * const ISO8601DateFormat = @"yyyy'-'MM'-'dd'T'HH':'mm':'ssZZZZZ";
return [MTLValueTransformer reversibleTransformerWithForwardBlock:^ NSDate * (NSString *dateString) {
if (![dateString isKindOfClass:NSString.class]) return nil;
@@ -134,19 +134,33 @@ NSString * const SQRLUpdateJSONPublicationDateKey = @"pub_date";
- (BOOL)validateUpdateURL:(NSURL **)updateURLPtr error:(NSError **)error {
NSURL *updateURL = *updateURLPtr;
if (![updateURL isKindOfClass:NSURL.class] || updateURL.scheme == nil || updateURL.host == nil || updateURL.path == nil) {
if (![updateURL isKindOfClass:NSURL.class]) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An invalid updateURL was given to SQRLUpdate: %@", nil), updateURL]
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
BOOL valid = (updateURL.scheme != nil);
valid &= ([updateURL.scheme isEqualToString:@"file"] || updateURL.host != nil);
valid &= (updateURL.path != nil);
if (!valid) {
if (error != NULL) {
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Validation failed", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"Update URLs must have a scheme, a host and a path: %@", nil), updateURL]
};
*error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSKeyValueValidationError userInfo:userInfo];
}
return NO;
}
return YES;
}
+66 -5
View File
@@ -6,6 +6,26 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
// Represents the current state of the updater.
//
// SQRLUpdaterStateIdle - Doing absolutely diddly squat.
// SQRLUpdaterStateCheckingForUpdate - Checking for any updates from the server.
// SQRLUpdaterStateDownloadingUpdate - Update found, downloading the archive.
// SQRLUpdaterStateAwaitingRelaunch - Awaiting a relaunch to install
// the update.
typedef enum : NSUInteger {
SQRLUpdaterStateIdle,
SQRLUpdaterStateCheckingForUpdate,
SQRLUpdaterStateDownloadingUpdate,
SQRLUpdaterStateAwaitingRelaunch,
} SQRLUpdaterState;
// Block for providing download requests given a download url
typedef NSURLRequest * (^SQRLRequestForDownload)(NSURL *);
// The domain for errors originating within SQRLUpdater.
extern NSString * const SQRLUpdaterErrorDomain;
@@ -25,6 +45,11 @@ extern const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement;
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidServerResponse;
// The server sent a response body that we didn't understand.
//
// Includes `SQRLUpdaterServerDataErrorKey` in the error's `userInfo`.
extern const NSInteger SQRLUpdaterErrorInvalidServerBody;
// The server sent update JSON that we didn't understand.
//
// Includes `SQRLUpdaterJSONObjectErrorKey` in the error's `userInfo`.
@@ -50,6 +75,11 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// 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.
//
@@ -57,11 +87,6 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// flattened for convenience.
@property (nonatomic, strong, readonly) RACSignal *updates;
// Whether or not to relaunch after installing an update.
//
// This will be reset to NO whenever update installation fails.
@property (atomic) BOOL shouldRelaunch;
// The request that will be sent to check for updates.
//
// The default value is the argument that was originally passed to
@@ -70,6 +95,15 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// This property must never be set to nil.
@property (atomic, copy) NSURLRequest *updateRequest;
// The block used for fetching a given download request
//
// The default value is the argument that was originally passed to
// -initWithUpdateRequest:requestForDownload:.
//
// If initialized with -initWithUpdateRequest: this block will
// return a generic NSURLRequest with the provided url.
@property (nonatomic, copy) SQRLRequestForDownload requestForDownload;
// The `SQRLUpdate` subclass to instantiate with the server's response.
//
// By default, this is `SQRLUpdate` itself, but it can be set to a custom
@@ -89,6 +123,18 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest;
// Initializes an updater that will send the given request to check for updates
// and passes a block to provide requests for the update downloads.
//
// updateRequest - Same as with initWithUpdateRequest
// requestForDownload - Once the update url is found for the update download, allow
// providing custom requests that can be costomized as desired.
// Useful for including `Authorization` headers just like the
// updateRequest param.
//
// Returns the initialized `SQRLUpdater`.
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload;
// Executes `checkForUpdatesCommand` (if enabled) every `interval` seconds.
//
// The first check will not occur until `interval` seconds have passed.
@@ -99,6 +145,21 @@ extern NSString * const SQRLUpdaterJSONObjectErrorKey;
// checking.
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval;
// Terminates the running application to install any available update, then
// automatically relaunches the app after updating.
//
// This method is only useful if you want the application to automatically
// relaunch. Otherwise, you can simply use `-[NSApplication terminate:]` or any
// other exit mechanism.
//
// After invoking this method, the receiver is responsible for terminating the
// application upon success. The app must not be terminated in any other way
// unless an error occurs.
//
// Returns a signal that will error on the main scheduler if anything goes
// wrong before termination. The signal will never complete.
- (RACSignal *)relaunchToInstallUpdate;
@end
@interface SQRLUpdater (Unavailable)
+308 -70
View File
@@ -15,11 +15,12 @@
#import "SQRLDirectoryManager.h"
#import "SQRLDownloadedUpdate.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLUpdate.h"
#import "SQRLZipArchiver.h"
#import "SQRLShipItRequest.h"
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <sys/mount.h>
NSString * const SQRLUpdaterErrorDomain = @"SQRLUpdaterErrorDomain";
NSString * const SQRLUpdaterServerDataErrorKey = @"SQRLUpdaterServerDataErrorKey";
@@ -30,9 +31,23 @@ const NSInteger SQRLUpdaterErrorPreparingUpdateJob = 3;
const NSInteger SQRLUpdaterErrorRetrievingCodeSigningRequirement = 4;
const NSInteger SQRLUpdaterErrorInvalidServerResponse = 5;
const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
const NSInteger SQRLUpdaterErrorInvalidServerBody = 7;
/// The application's being run on a read-only volume.
const NSInteger SQRLUpdaterErrorReadOnlyVolume = 8;
// The prefix used when creating temporary directories for updates. This will be
// followed by a random string of characters.
static NSString * const SQRLUpdaterUniqueTemporaryDirectoryPrefix = @"update.";
@interface SQRLUpdater ()
@property (atomic, readwrite) SQRLUpdaterState state;
/// The etag of the currently downloaded update, nil if no update has been
/// downloaded.
@property (atomic, copy) NSString *etag;
// The code signature for the running application, used to check updates before
// sending them to ShipIt.
@property (nonatomic, strong, readonly) SQRLCodeSignature *signature;
@@ -129,21 +144,37 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest {
return [self initWithUpdateRequest:updateRequest requestForDownload:^(NSURL *downloadURL) {
return [NSURLRequest requestWithURL:downloadURL];
}];
}
- (id)initWithUpdateRequest:(NSURLRequest *)updateRequest requestForDownload:(SQRLRequestForDownload)requestForDownload {
NSParameterAssert(updateRequest != nil);
NSParameterAssert(requestForDownload != nil);
self = [super init];
if (self == nil) return nil;
_requestForDownload = [requestForDownload copy];
_updateRequest = [updateRequest copy];
_updateClass = SQRLUpdate.class;
NSError *error = nil;
_signature = [SQRLCodeSignature currentApplicationSignature:&error];
NSAssert(_signature != nil, @"Could not get code signature for running application: %@", error);
if (_signature == nil) {
#if DEBUG
NSLog(@"Could not get code signature for running application, application updates are disabled: %@", error);
return nil;
#else
NSDictionary *exceptionInfo = @{ NSUnderlyingErrorKey: error };
@throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Could not get code signature for running application" userInfo:exceptionInfo];
#endif
}
BOOL updatesDisabled = (getenv("DISABLE_UPDATE_CHECK") != NULL);
@weakify(self);
_checkForUpdatesCommand = [[RACCommand alloc] initWithEnabled:[RACSignal return:@(!updatesDisabled)] signalBlock:^(id _) {
@strongify(self);
NSParameterAssert(self.updateRequest != nil);
@@ -152,34 +183,74 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
NSMutableURLRequest *request = [self.updateRequest mutableCopy];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
return [[[[[[NSURLConnection
rac_sendAsynchronousRequest:request]
reduceEach:^(id _, NSData *data) {
return data;
return [[[[[[[[self
performHousekeeping]
then:^{
self.state = SQRLUpdaterStateCheckingForUpdate;
return [NSURLConnection rac_sendAsynchronousRequest:request];
}]
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update check failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
if (httpResponse.statusCode == 204 /* No Content */) {
return [RACSignal empty];
}
BOOL readOnlyVolume = [self isRunningOnReadOnlyVolume];
if (readOnlyVolume) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Cannot update while running on a read-only volume", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The application is on a read-only volume. Please move the application and try again. If you're on macOS Sierra or later, you'll need to move the application out of the Downloads directory. See https://github.com/Squirrel/Squirrel.Mac/issues/182 for more information.", nil),
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorReadOnlyVolume userInfo:errorInfo];
return [RACSignal error:error];
}
}
return [RACSignal return:bodyData];
}]
flatten]
flattenMap:^(NSData *data) {
return [self updateFromJSONData:data];
}]
flattenMap:^(SQRLUpdate *update) {
return [self downloadAndPrepareUpdate:update];
return [[RACSignal
defer:^{
self.state = SQRLUpdaterStateDownloadingUpdate;
return [self downloadAndPrepareUpdate:update];
}]
doCompleted:^{
self.state = SQRLUpdaterStateAwaitingRelaunch;
}];
}]
doError:^(id _) {
self.shouldRelaunch = NO;
finally:^{
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return;
self.state = SQRLUpdaterStateIdle;
}]
deliverOn:RACScheduler.mainThreadScheduler];
}];
_shipItLauncher = [[[RACSignal
defer:^{
@strongify(self);
NSURL *targetURL = NSRunningApplication.currentApplication.bundleURL;
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.
return [SQRLShipItLauncher launchPrivileged:(gotWritable && !targetWritable.boolValue)];
BOOL targetWritable = [self canWriteToURL:targetURL];
BOOL parentWritable = [self canWriteToURL:targetURL.URLByDeletingLastPathComponent];
return [SQRLShipItLauncher launchPrivileged:!targetWritable || !parentWritable];
}]
replayLazily]
setNameWithFormat:@"shipItLauncher"];
@@ -187,6 +258,15 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
return self;
}
- (BOOL)canWriteToURL:(NSURL *)fileURL {
NSNumber *writable = nil;
NSError *writableError = nil;
BOOL gotWritable = [fileURL getResourceValue:&writable forKey:NSURLIsWritableKey error:&writableError];
// If we can't determine whether it can be written, assume nonprivileged and
// wait for another, more canonical error.
return !gotWritable || writable.boolValue;
}
#pragma mark Checking for Updates
- (RACDisposable *)startAutomaticChecksWithInterval:(NSTimeInterval)interval {
@@ -222,7 +302,7 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
userInfo[SQRLUpdaterServerDataErrorKey] = data;
if (error != nil) userInfo[NSUnderlyingErrorKey] = error;
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:userInfo]];
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerBody userInfo:userInfo]];
}
Class updateClass = self.updateClass;
@@ -253,55 +333,104 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
return [[[self
uniqueTemporaryDirectoryForUpdate]
flattenMap:^(NSURL *downloadDirectory) {
void (^cleanUp)(void) = ^{
NSError *error;
if (![NSFileManager.defaultManager removeItemAtURL:downloadDirectory error:&error]) {
NSLog(@"Error removing temporary download directory at %@: %@", downloadDirectory, error.sqrl_verboseDescription);
}
};
return [[[self
downloadBundleForUpdate:update intoDirectory:downloadDirectory]
flattenMap:^(NSBundle *updateBundle) {
// If the bundle is nil it means our conditional GET told us
// we already downloaded the update. So just clean up.
if (updateBundle == nil) {
cleanUp();
return [RACSignal empty];
}
return [self verifyAndPrepareUpdate:update fromBundle:updateBundle];
}]
doError:^(id _) {
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:downloadDirectory error:&error]) {
NSLog(@"Error removing temporary download directory at %@: %@", downloadDirectory, error.sqrl_verboseDescription);
}
cleanUp();
}];
}]
setNameWithFormat:@"%@ -downloadAndPrepareUpdate: %@", self, update];
}
- (RACSignal *)unarchiveAndPrepareData:(NSData *)data withName:(NSString *)name intoDirectory:(NSURL *)downloadDirectory {
return [[[[[RACSignal
defer:^{
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:name];
NSError *error = nil;
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal return:zipOutputURL];
} else {
return [RACSignal error:error];
}
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [[[[SQRLZipArchiver
unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory]
ignoreValues]
concat:[RACSignal return:zipOutputURL]]
doCompleted:^{
NSError *error = nil;
if (![NSFileManager.defaultManager removeItemAtURL:zipOutputURL error:&error]) {
NSLog(@"Error removing downloaded archive at %@: %@", zipOutputURL, error.sqrl_verboseDescription);
}
}];
}]
flattenMap:^(NSURL *zipOutputURL) {
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
}]
setNameWithFormat:@"%@ -unarchiveAndPrepareData:withName: %@ intoDirectory: %@", self, name, downloadDirectory];
}
- (RACSignal *)downloadBundleForUpdate:(SQRLUpdate *)update intoDirectory:(NSURL *)downloadDirectory {
NSParameterAssert(update != nil);
NSParameterAssert(downloadDirectory != nil);
return [[[[[RACSignal
return [[RACSignal
defer:^{
NSURL *zipDownloadURL = update.updateURL;
NSMutableURLRequest *zipDownloadRequest = [NSMutableURLRequest requestWithURL:zipDownloadURL];
NSMutableURLRequest *zipDownloadRequest = [self.requestForDownload(zipDownloadURL) mutableCopy];
[zipDownloadRequest setValue:@"application/zip" forHTTPHeaderField:@"Accept"];
if (self.etag != nil) {
[zipDownloadRequest setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
return [[[NSURLConnection
rac_sendAsynchronousRequest:zipDownloadRequest]
reduceEach:^(id _, NSData *data) {
return data;
}]
flattenMap:^(NSData *data) {
NSURL *zipOutputURL = [downloadDirectory URLByAppendingPathComponent:zipDownloadURL.lastPathComponent];
reduceEach:^(NSURLResponse *response, NSData *bodyData) {
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSHTTPURLResponse *httpResponse = (id)response;
NSError *error = nil;
if ([data writeToURL:zipOutputURL options:NSDataWritingAtomic error:&error]) {
return [RACSignal return:zipOutputURL];
} else {
return [RACSignal error:error];
if (httpResponse.statusCode == 304 /* Not Modified */) {
return [RACSignal return:nil];
}
if (!(httpResponse.statusCode >= 200 && httpResponse.statusCode <= 299)) {
NSDictionary *errorInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Update download failed", nil),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"The server sent an invalid response. Try again later.", nil),
SQRLUpdaterServerDataErrorKey: bodyData,
};
NSError *error = [NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorInvalidServerResponse userInfo:errorInfo];
return [RACSignal error:error];
}
self.etag = httpResponse.allHeaderFields[@"ETag"];
}
}];
}]
doNext:^(NSURL *zipOutputURL) {
NSLog(@"Download completed to: %@", zipOutputURL);
}]
flattenMap:^(NSURL *zipOutputURL) {
return [SQRLZipArchiver unzipArchiveAtURL:zipOutputURL intoDirectoryAtURL:downloadDirectory];
}]
then:^{
return [self updateBundleMatchingCurrentApplicationInDirectory:downloadDirectory];
return [self unarchiveAndPrepareData:bodyData withName:zipDownloadURL.lastPathComponent intoDirectory:downloadDirectory];
}]
flatten];
}]
setNameWithFormat:@"%@ -downloadBundleForUpdate: %@ intoDirectory: %@", self, update, downloadDirectory];
}
@@ -312,10 +441,10 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
return [[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager applicationSupportURL];
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *appSupportURL) {
NSURL *updateDirectoryTemplate = [appSupportURL URLByAppendingPathComponent:@"update.XXXXXXX"];
flattenMap:^(NSURL *storageURL) {
NSURL *updateDirectoryTemplate = [storageURL URLByAppendingPathComponent:[SQRLUpdaterUniqueTemporaryDirectoryPrefix stringByAppendingString:@"XXXXXXX"]];
char *updateDirectoryCString = strdup(updateDirectoryTemplate.path.fileSystemRepresentation);
@onExit {
free(updateDirectoryCString);
@@ -384,6 +513,109 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
setNameWithFormat:@"%@ -applicationBundleMatchingCurrentApplicationInDirectory: %@", self, directory];
}
- (RACSignal *)shipItStateURL {
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return directoryManager.shipItStateURL;
}]
setNameWithFormat:@"%@ -shipItStateURL", self];
}
/// Is the host app running on a read-only volume?
- (BOOL)isRunningOnReadOnlyVolume {
struct statfs statfsInfo;
NSURL *bundleURL = NSRunningApplication.currentApplication.bundleURL;
int result = statfs(bundleURL.fileSystemRepresentation, &statfsInfo);
if (result == 0) {
return (statfsInfo.f_flags & MNT_RDONLY) != 0;
} else {
// If we can't even check if the volume is read-only, assume it is.
return true;
}
}
- (RACSignal *)performHousekeeping {
return [[RACSignal
merge:@[ [self pruneUpdateDirectories], [self truncateLogs] ]]
catch:^(NSError *error) {
NSLog(@"Error doing housekeeping: %@", error);
return [RACSignal empty];
}];
}
/// Lazily removes outdated temporary directories (used for previous updates)
/// upon subscription.
///
/// Pruning directories while an update is pending or in progress will result in
/// undefined behavior.
///
/// Sends each removed directory then completes, or errors, on an unspecified
/// thread.
- (RACSignal *)pruneUpdateDirectories {
return [[[RACSignal
defer:^{
// If we already have updates downloaded we don't wanna prune them.
if (self.state == SQRLUpdaterStateAwaitingRelaunch) return [RACSignal empty];
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [directoryManager storageURL];
}]
flattenMap:^(NSURL *storageURL) {
NSFileManager *manager = [[NSFileManager alloc] init];
NSDirectoryEnumerator *enumerator = [manager enumeratorAtURL:storageURL includingPropertiesForKeys:nil options:NSDirectoryEnumerationSkipsSubdirectoryDescendants errorHandler:^(NSURL *URL, NSError *error) {
NSLog(@"Error enumerating item %@ within directory %@: %@", URL, storageURL, error);
return YES;
}];
return [[enumerator.rac_sequence.signal
filter:^(NSURL *enumeratedURL) {
NSString *name = enumeratedURL.lastPathComponent;
return [name hasPrefix:SQRLUpdaterUniqueTemporaryDirectoryPrefix];
}]
doNext:^(NSURL *directoryURL) {
NSError *error = nil;
if (![manager removeItemAtURL:directoryURL error:&error]) {
NSLog(@"Error removing old update directory at %@: %@", directoryURL, error.sqrl_verboseDescription);
}
}];
}]
setNameWithFormat:@"%@ -prunedUpdateDirectories", self];
}
// Like truncation, but backwards.
- (RACSignal *)backwardTruncateFile:(NSURL *)fileURL {
return [RACSignal defer:^{
static const NSInteger MAX_LENGTH = 1024 * 1024 * 8;
NSError *error;
NSFileHandle *handle = [NSFileHandle fileHandleForWritingToURL:fileURL error:&error];
if (handle == nil) return [RACSignal error:error];
unsigned long long fileLength = [handle seekToEndOfFile];
if (fileLength <= MAX_LENGTH) return [RACSignal empty];
[handle seekToFileOffset:fileLength - MAX_LENGTH];
NSData *mostRecentData = [handle readDataToEndOfFile];
[handle truncateFileAtOffset:0];
[handle writeData:mostRecentData];
return [RACSignal empty];
}];
}
- (RACSignal *)truncateLogs {
return [[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
return [RACSignal zip:@[ [directoryManager shipItStdoutURL], [directoryManager shipItStderrURL] ]];
}]
reduceEach:^(NSURL *stdoutURL, NSURL *stderrURL) {
return [RACSignal merge:@[ [self backwardTruncateFile:stdoutURL], [self backwardTruncateFile:stderrURL] ]];
}];
}
#pragma mark Installing Updates
- (RACSignal *)verifyAndPrepareUpdate:(SQRLUpdate *)update fromBundle:(NSBundle *)updateBundle {
@@ -409,30 +641,14 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
return [[[[RACSignal
defer:^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:SQRLShipItLauncher.shipItJobLabel];
RACSignal *stateLocation = directoryManager.shipItStateURL;
return [[[[SQRLShipItState
readUsingURL:stateLocation]
catchTo:[RACSignal empty]]
flattenMap:^(SQRLShipItState *existingState) {
if (existingState.installerState != SQRLInstallerStateNothingToDo) {
// If this happens, shit is crazy, because it implies that an
// update is being installed over us right now.
NSDictionary *userInfo = @{
NSLocalizedDescriptionKey: NSLocalizedString(@"Installation in progress", nil),
NSLocalizedRecoverySuggestionErrorKey: [NSString stringWithFormat:NSLocalizedString(@"An update for %@ is already in progress.", nil), NSRunningApplication.currentApplication.bundleIdentifier],
};
NSRunningApplication *currentApplication = NSRunningApplication.currentApplication;
NSBundle *appBundle = [NSBundle bundleWithURL:currentApplication.bundleURL];
// Only use the update bundle's name if the user hasn't renamed the
// app themselves.
BOOL useUpdateBundleName = [appBundle.sqrl_executableName isEqual:currentApplication.bundleURL.lastPathComponent.stringByDeletingPathExtension];
return [RACSignal error:[NSError errorWithDomain:SQRLUpdaterErrorDomain code:SQRLUpdaterErrorPreparingUpdateJob userInfo:userInfo]];
}
return [RACSignal empty];
}]
then:^{
SQRLShipItState *state = [[SQRLShipItState alloc] initWithTargetBundleURL:NSRunningApplication.currentApplication.bundleURL updateBundleURL:update.bundle.bundleURL bundleIdentifier:NSRunningApplication.currentApplication.bundleIdentifier codeSignature:self.signature];
state.relaunchAfterInstallation = self.shouldRelaunch;
return [state writeUsingURL:stateLocation];
}];
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:update.bundle.bundleURL targetBundleURL:currentApplication.bundleURL bundleIdentifier:currentApplication.bundleIdentifier launchAfterInstallation:NO useUpdateBundleName:useUpdateBundleName];
return [request writeUsingURL:self.shipItStateURL];
}]
then:^{
return self.shipItLauncher;
@@ -441,4 +657,26 @@ const NSInteger SQRLUpdaterErrorInvalidJSON = 6;
setNameWithFormat:@"%@ -prepareUpdateForInstallation: %@", self, update];
}
- (RACSignal *)relaunchToInstallUpdate {
return [[[[[[[[SQRLShipItRequest
readUsingURL:self.shipItStateURL]
map:^(SQRLShipItRequest *request) {
return [[SQRLShipItRequest alloc] initWithUpdateBundleURL:request.updateBundleURL targetBundleURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier launchAfterInstallation:YES useUpdateBundleName:request.useUpdateBundleName];
}]
flattenMap:^(SQRLShipItRequest *request) {
return [[request
writeUsingURL:self.shipItStateURL]
sqrl_addTransactionWithName:NSLocalizedString(@"Preparing to relaunch", nil) description:NSLocalizedString(@"%@ is preparing to relaunch to install an update. Interrupting the process could corrupt the application.", nil), NSRunningApplication.currentApplication.bundleIdentifier];
}]
deliverOn:RACScheduler.mainThreadScheduler]
doCompleted:^{
[NSApp terminate:self];
}]
// Never allow `completed` to escape this signal chain (in case
// -terminate: is asynchronous or something crazy).
concat:[RACSignal never]]
replay]
setNameWithFormat:@"%@ -relaunchToInstallUpdate", self];
}
@end
+114 -77
View File
@@ -7,15 +7,18 @@
//
#import <Foundation/Foundation.h>
#import <ReactiveCocoa/EXTScope.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import "EXTScope.h"
#import <ReactiveCocoa/RACCommand.h>
#import <ReactiveCocoa/RACSignal+Operations.h>
#import <ReactiveCocoa/RACScheduler.h>
#import "NSError+SQRLVerbosityExtensions.h"
#import "RACSignal+SQRLTransactionExtensions.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItState.h"
#import "SQRLInstaller+Private.h"
#import "SQRLTerminationListener.h"
#import "SQRLShipItRequest.h"
// The maximum number of times ShipIt should run the same installation state, in
// an attempt to update.
@@ -27,99 +30,133 @@ static const NSUInteger SQRLShipItMaximumInstallationAttempts = 3;
// The domain for errors generated here.
static NSString * const SQRLShipItErrorDomain = @"SQRLShipItErrorDomain";
static NSUInteger installationAttempts(NSString *applicationIdentifier) {
return CFPreferencesGetAppIntegerValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFStringRef)applicationIdentifier, NULL);
}
static BOOL setInstallationAttempts(NSString *applicationIdentifier, NSUInteger attempts) {
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(attempts), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
}
static BOOL clearInstallationAttempts(NSString *applicationIdentifier) {
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, NULL, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
return CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
}
// Waits for all instances of the target application (as described in the
// `state`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItState *state) {
// `request`) to exit, then sends completed.
static RACSignal *waitForTerminationIfNecessary(SQRLShipItRequest *request) {
return [[RACSignal
defer:^{
if (state.bundleIdentifier == nil) return [RACSignal empty];
if (request.bundleIdentifier == nil) return [RACSignal empty];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:state.targetBundleURL bundleIdentifier:state.bundleIdentifier];
SQRLTerminationListener *listener = [[SQRLTerminationListener alloc] initWithURL:request.targetBundleURL bundleIdentifier:request.bundleIdentifier];
return [listener waitForTermination];
}]
setNameWithFormat:@"waitForTerminationIfNecessary"];
}
static void installRequest(RACSignal *readRequestSignal, NSString *applicationIdentifier) {
[[[[[readRequestSignal
flattenMap:^(SQRLShipItRequest *request) {
return waitForTerminationIfNecessary(request);
}]
ignoreValues]
concat:readRequestSignal]
flattenMap:^(SQRLShipItRequest *request) {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:applicationIdentifier];
NSUInteger attempt = installationAttempts(applicationIdentifier) + 1;
setInstallationAttempts(applicationIdentifier, attempt);
RACSignal *action;
if (attempt > SQRLShipItMaximumInstallationAttempts) {
action = [[[[installer.abortInstallationCommand
execute:request]
initially:^{
NSLog(@"Too many attempts to install, aborting update");
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
// Exit successfully so launchd doesn't restart us
// again.
return [RACSignal empty];
}]
concat:[RACSignal return:request]];
} else {
action = [[[[installer.installUpdateCommand
execute:request]
initially:^{
BOOL freshInstall = (attempt == 1);
if (freshInstall) {
NSLog(@"Beginning installation");
} else {
NSLog(@"Resuming installation attempt %i", (int)attempt);
}
}]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), request.targetBundleURL.path];
}
// Clear the installation attempts for a successful abort or
// install.
action = [action doCompleted:^{
clearInstallationAttempts(applicationIdentifier);
}];
if (request.launchAfterInstallation) {
// Launch regardless of whether installation succeeds or fails.
action = [[action
deliverOn:RACScheduler.mainThreadScheduler]
doNext:^(SQRLShipItRequest *finalRequest) {
NSURL *bundleURL = finalRequest.targetBundleURL;
if (bundleURL == nil) {
NSLog(@"Missing target bundle URL, cannot launch application");
return;
}
NSError *error;
if (![NSWorkspace.sharedWorkspace launchApplicationAtURL:bundleURL options:NSWorkspaceLaunchDefault configuration:@{} error:&error]) {
NSLog(@"Could not launch application at %@: %@", bundleURL, error);
return;
}
NSLog(@"Application launched at %@", bundleURL);
}];
}
return action;
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
atexit_b(^{
NSLog(@"ShipIt quitting");
});
if (argc < 2) {
NSLog(@"Missing launchd job label for ShipIt");
if (argc < 3) {
NSLog(@"Missing launchd job label or state path 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);
}]
then:^{
// Read the latest state, in case it was modified by the
// controlling application in the meantime.
return [SQRLShipItState readUsingURL:stateLocation];
}]
catch:^(NSError *error) {
NSLog(@"Error reading saved installer state: %@", error);
// 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);
if (attempt > SQRLShipItMaximumInstallationAttempts) {
return [[[installer.abortInstallationCommand
execute:state]
initially:^{
NSLog(@"Too many attempts to install from state %i, aborting update", (int)state.installerState);
}]
catch:^(NSError *error) {
NSLog(@"Error aborting installation: %@", error);
// Exit successfully so launchd doesn't restart us again.
return [RACSignal empty];
}];
} else {
return [[[[[state
writeUsingURL:stateLocation]
initially:^{
if (freshInstall) {
NSLog(@"Beginning installation");
state.installerState = SQRLInstallerStateClearingQuarantine;
} else {
NSLog(@"Resuming installation from state %i", (int)state.installerState);
}
state.installationStateAttempt = attempt;
}]
then:^{
return [installer.installUpdateCommand execute:state];
}]
doCompleted:^{
NSLog(@"Installation completed successfully");
}]
sqrl_addTransactionWithName:NSLocalizedString(@"Updating", nil) description:NSLocalizedString(@"%@ is being updated, and interrupting the process could corrupt the application", nil), state.targetBundleURL.path];
}
}]
subscribeError:^(NSError *error) {
NSLog(@"Installation error: %@", error);
exit(EXIT_FAILURE);
} completed:^{
exit(EXIT_SUCCESS);
}];
char const *jobLabel = argv[1];
const char *statePath = argv[2];
NSURL *shipItStateURL = [NSURL fileURLWithPath:@(statePath)];
installRequest([SQRLShipItRequest readUsingURL:[RACSignal return:shipItStateURL]], @(jobLabel));
dispatch_main();
}
return EXIT_SUCCESS;
}
+1 -1
View File
@@ -9,7 +9,7 @@
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
-8
View File
@@ -1,8 +0,0 @@
//
// Prefix header for all source files of the 'Squirrel' target in the 'Squirrel' project
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SystemConfiguration/SystemConfiguration.h>
#endif
+8
View File
@@ -6,6 +6,14 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Squirrel.
FOUNDATION_EXPORT double SquirrelVersionNumber;
//! Project version string for Squirrel.
FOUNDATION_EXPORT const unsigned char SquirrelVersionString[];
#import <Squirrel/NSBundle+SQRLVersionExtensions.h>
#import <Squirrel/NSProcessInfo+SQRLVersionExtensions.h>
#import <Squirrel/SQRLDownloadedUpdate.h>
-2
View File
@@ -1,2 +0,0 @@
/* Localized versions of Info.plist keys */
+12 -7
View File
@@ -6,30 +6,35 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(NSProcessInfoExtensions)
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(NSProcessInfoExtensions)
describe(@"-sqrl_operatingSystemShortVersionString", ^{
__block NSString *versionString;
beforeEach(^{
versionString = NSProcessInfo.processInfo.sqrl_operatingSystemShortVersionString;
expect(versionString).notTo.beNil();
expect(versionString).notTo(beNil());
});
it(@"should follow major.minor.patch format", ^{
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"^[0-9]+(\\.[0-9]+)*$" options:0 error:&error];
expect(regex).notTo.beNil();
expect(error).to.beNil();
expect(regex).notTo(beNil());
expect(error).to(beNil());
NSRange fullRange = NSMakeRange(0, versionString.length);
NSRange matchRange = [regex rangeOfFirstMatchInString:versionString options:0 range:fullRange];
expect(NSEqualRanges(matchRange, fullRange)).to.beTruthy();
expect(@(NSEqualRanges(matchRange, fullRange))).to(beTruthy());
});
it(@"should be numerically comparable", ^{
expect([@"10.6.9" compare:versionString options:NSNumericSearch]).to.equal(NSOrderedAscending);
expect(@([@"10.6.9" compare:versionString options:NSNumericSearch])).to(equal(@(NSOrderedAscending)));
});
});
SpecEnd
QuickSpecEnd
@@ -1,11 +1,19 @@
//
// SQRLTestCase.h
// QuickSpec+SQRLFixtures.h
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
/// See https://github.com/Squirrel/Squirrel.Mac/pull/154#issuecomment-151287744
#define SKIP_IF_RUNNING_ON_TRAVIS if (self.runningOnTravis) return;
// The short version string for the `testApplicationBundle`.
extern NSString * const SQRLTestApplicationOriginalShortVersionString;
@@ -16,10 +24,15 @@ extern NSString * const SQRLTestApplicationUpdatedShortVersionString;
// The Info.plist key associated with a bundle's short version string.
extern NSString * const SQRLBundleShortVersionStringKey;
/// The longer timeout we should use for things that take a long time. This is
/// especially needed on slower machines (e.g., Travis CI).
extern const NSTimeInterval SQRLLongTimeout;
@class SQRLCodeSignature;
@class SQRLDirectoryManager;
@class SQRLShipItRequest;
@interface SQRLTestCase : SPTSenTestCase
@interface QuickSpec (SQRLFixtures)
// A URL to a temporary directory tests can use.
//
@@ -45,13 +58,12 @@ 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;
/// Are the tests currently being run on Travis?
@property (nonatomic, readonly, assign, getter = isRunningOnTravis) BOOL runningOnTravis;
// Launches a new copy of TestApplication.app from the `testApplicationBundle`.
//
// Invoking this method multiple times will result in multiple running instances
@@ -74,8 +86,8 @@ 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.
- (void)installWithRequest:(SQRLShipItRequest *)request remote:(BOOL)remote;
// Creates a disk image, then mounts it.
//
@@ -88,4 +100,11 @@ extern NSString * const SQRLBundleShortVersionStringKey;
// automatically be unmounted and deleted at the end of the example.
- (NSURL *)createAndMountDiskImageNamed:(NSString *)name fromDirectory:(NSURL *)directoryURL;
// Add a block to cleanup after each example has run
//
// Blocks are run in reverse order, i.e. LIFO
//
// block - The block to invoke after the current example has finished
- (void)addCleanupBlock:(dispatch_block_t)block;
@end
@@ -1,16 +1,20 @@
//
// SQRLTestCase.m
// QuickSpec+SQRLFixtures.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-08-06.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTestCase.h"
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItRequest.h"
#import <ServiceManagement/ServiceManagement.h>
#import <objc/runtime.h>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-retain-cycles"
@@ -19,6 +23,8 @@ NSString * const SQRLTestApplicationOriginalShortVersionString = @"1.0";
NSString * const SQRLTestApplicationUpdatedShortVersionString = @"2.1";
NSString * const SQRLBundleShortVersionStringKey = @"CFBundleShortVersionString";
const NSTimeInterval SQRLLongTimeout = 20;
static void SQRLKillAllTestApplications(void) {
// Forcibly kill all copies of the TestApplication that may be running.
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"];
@@ -39,36 +45,25 @@ static void SQRLSignalHandler(int sig) {
abort();
}
@interface SQRLTestCase ()
static NSBundle *SQRLTestBundle(void) {
return [NSBundle bundleWithIdentifier:@"com.github.SquirrelTests"];
}
// An array of dispatch_block_t values, for performing cleanup after the current
// example finishes.
//
// This array will be emptied between each example.
@property (nonatomic, strong, readonly) NSMutableArray *exampleCleanupBlocks;
static NSMutableArray *cleanupBlocks = nil;
// The URL to the temporary directory which contains `temporaryDirectoryURL` and
// all copied test data.
@property (nonatomic, copy, readonly) NSURL *baseTemporaryDirectoryURL;
static NSURL *baseTemporaryDirectoryURL = nil;
@end
QuickConfigurationBegin(Fixtures)
@implementation SQRLTestCase
#pragma mark Properties
@synthesize baseTemporaryDirectoryURL = _baseTemporaryDirectoryURL;
#pragma mark Lifecycle
+ (void)load {
+ (void)configure:(Configuration *)configuration {
NSURL *appSupportURL = [NSFileManager.defaultManager URLForDirectory:NSApplicationSupportDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:YES error:NULL];
NSAssert(appSupportURL != nil, @"Could not find Application Support folder");
NSArray *folders = @[
@"com.github.Squirrel.TestApplication.ShipIt",
@"otest.ShipIt",
@"otest-x86_64.ShipIt",
@"com.github.Squirrel.SquirrelTests.ShipIt",
];
RACSequence *URLs = [folders.rac_sequence flattenMap:^(NSString *folder) {
@@ -79,76 +74,69 @@ static void SQRLSignalHandler(int sig) {
].rac_sequence;
}];
for (NSURL *URL in URLs) {
[[NSData data] writeToURL:URL atomically:YES];
}
[configuration beforeSuite:^{
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
NSArray *args = [[[URLs
map:^(NSURL *URL) {
return URL.path;
}]
startWith:@"-f"]
array];
cleanupBlocks = [NSMutableArray array];
NSTask *readShipIt = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:args];
NSAssert([readShipIt isRunning], @"Could not start task %@ with arguments: %@", readShipIt, args);
for (NSURL *URL in URLs) {
[NSFileManager.defaultManager createDirectoryAtURL:URL.URLByDeletingLastPathComponent withIntermediateDirectories:YES attributes:nil error:NULL];
if (![[NSData data] writeToURL:URL atomically:YES]) {
NSLog(@"Could not touch log file at %@", URL);
}
}
}];
atexit_b(^{
[readShipIt terminate];
});
// We want to run any enqueued cleanup blocks after _and before_ each spec,
// in case beforeSuites (for example) use the fixtures.
void (^runCleanupBlocks)(void) = ^{
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
block();
}
[cleanupBlocks removeAllObjects];
};
[configuration beforeEach:^{
runCleanupBlocks();
}];
[configuration afterEach:^{
runCleanupBlocks();
SQRLKillAllTestApplications();
}];
}
- (void)setUp {
[super setUp];
QuickConfigurationEnd
signal(SIGILL, &SQRLSignalHandler);
NSSetUncaughtExceptionHandler(&SQRLUncaughtExceptionHandler);
@implementation QuickSpec (SQRLFixtures)
Expecta.asynchronousTestTimeout = 3;
}
- (void)SPT_setUp {
_exampleCleanupBlocks = [[NSMutableArray alloc] init];
}
- (void)SPT_tearDown {
NSArray *cleanupBlocks = [self.exampleCleanupBlocks copy];
_exampleCleanupBlocks = nil;
// Enumerate backwards, so later resources are cleaned up first.
for (dispatch_block_t block in cleanupBlocks.reverseObjectEnumerator) {
block();
}
}
- (void)tearDown {
[super tearDown];
SQRLKillAllTestApplications();
}
#pragma mark Lifecycle
- (void)addCleanupBlock:(dispatch_block_t)block {
[self.exampleCleanupBlocks addObject:[block copy]];
[cleanupBlocks addObject:[block copy]];
}
#pragma mark Temporary Directory
- (NSURL *)baseTemporaryDirectoryURL {
if (_baseTemporaryDirectoryURL == nil) {
if (baseTemporaryDirectoryURL == nil) {
NSURL *globalTemporaryDirectory = [NSURL fileURLWithPath:NSTemporaryDirectory() isDirectory:YES];
_baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
baseTemporaryDirectoryURL = [[globalTemporaryDirectory URLByAppendingPathComponent:@"com.github.SquirrelTests"] URLByAppendingPathComponent:[NSProcessInfo.processInfo globallyUniqueString]];
NSError *error = nil;
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:_baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
STAssertTrue(success, @"Couldn't create temporary directory at %@: %@", _baseTemporaryDirectoryURL, error);
BOOL success = [NSFileManager.defaultManager createDirectoryAtURL:baseTemporaryDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error];
XCTAssertTrue(success, @"Couldn't create temporary directory at %@: %@", baseTemporaryDirectoryURL, error);
[self addCleanupBlock:^{
[NSFileManager.defaultManager removeItemAtURL:_baseTemporaryDirectoryURL error:NULL];
_baseTemporaryDirectoryURL = nil;
[NSFileManager.defaultManager removeItemAtURL:baseTemporaryDirectoryURL error:NULL];
baseTemporaryDirectoryURL = nil;
}];
}
return _baseTemporaryDirectoryURL;
return baseTemporaryDirectoryURL;
}
- (NSURL *)temporaryDirectoryURL {
@@ -156,7 +144,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;
}
@@ -166,22 +154,15 @@ static void SQRLSignalHandler(int sig) {
- (NSURL *)testApplicationURL {
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");
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
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 = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/tail" arguments:@[ @"-f", testAppLog.path ]];
STAssertTrue([readTestApp isRunning], @"Could not start task %@ to read %@", readTestApp, testAppLog);
[self addCleanupBlock:^{
[readTestApp terminate];
}];
}
return fixtureURL;
@@ -190,8 +171,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;
}
@@ -210,7 +191,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) {
@@ -230,12 +211,12 @@ 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");
NSURL *originalURL = [SQRLTestBundle() URLForResource:@"TestApplication 2.1" withExtension:@"app"];
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];
@@ -243,18 +224,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");
NSURL *bundleURL = [SQRLTestBundle() URLForResource:@"TestApplication" withExtension:@"app"];
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);
@@ -262,7 +243,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);
@@ -272,52 +253,57 @@ static void SQRLSignalHandler(int sig) {
}
- (SQRLCodeSignature *)testApplicationSignature {
return [self performWithTestApplicationRequirement:^(SecRequirementRef requirement) {
return [[SQRLCodeSignature alloc] initWithRequirement:requirement];
}];
}
NSURL *bundleURL = [SQRLTestBundle() 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;
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) {
expect(@([[request writeUsingURL:self.shipItDirectoryManager.shipItStateURL] waitUntilCompleted:NULL])).to(beTruthy());
[self addCleanupBlock:^{
// Remove ShipIt's launchd job so it doesn't relaunch itself.
CFErrorRef removeError = NULL;
if (!SMJobRemove(kSMDomainUserLaunchd, (__bridge CFStringRef)SQRLShipItLauncher.shipItJobLabel, NULL, true, &removeError)) {
NSLog(@"Could not remove ShipIt job after tests: %@", removeError);
if (removeError != NULL) CFRelease(removeError);
}
__block NSError *error = nil;
expect(@([[SQRLShipItLauncher launchPrivileged:NO] 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)SQRLShipItLauncher.shipItJobLabel, NULL, true, NULL);
NSError *lookupError;
NSURL *stateURL = [[self.shipItDirectoryManager shipItStateURL] firstOrDefault:nil success:NULL error:&lookupError];
expect(stateURL).notTo(beNil());
expect(lookupError).to(beNil());
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
}];
} else {
SQRLInstaller *installer = [[SQRLInstaller alloc] initWithApplicationIdentifier:self.shipItDirectoryManager.applicationIdentifier];
expect(installer).notTo(beNil());
NSError *installedError = nil;
BOOL installed = [[installer.installUpdateCommand execute:request] asynchronouslyWaitUntilCompleted:&installedError];
expect(@(installed)).to(beTruthy());
expect(installedError).to(beNil());
}
}
- (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) {
@@ -326,20 +312,24 @@ static void SQRLSignalHandler(int sig) {
createInvocation = [NSString stringWithFormat:@"hdiutil create '%@' -fs 'HFS+' -volname '%@' -format UDSP -size 10m -srcfolder '%@' -quiet", destinationURL.path, name, directoryURL.path];
}
expect(system(createInvocation.UTF8String)).to.equal(0);
expect(@(system(createInvocation.UTF8String))).to(equal(@0));
NSString *mountInvocation = [NSString stringWithFormat:@"hdiutil attach '%@.sparseimage' -noverify -noautofsck -readwrite -quiet", destinationURL.path];
expect(system(mountInvocation.UTF8String)).to.equal(0);
expect(@(system(mountInvocation.UTF8String))).to(equal(@0));
NSString *path = [NSString stringWithFormat:@"/Volumes/%@", name];
[self addCleanupBlock:^{
NSString *detachInvocation = [NSString stringWithFormat:@"hdiutil detach '%@' -force -quiet", path];
expect(system(detachInvocation.UTF8String)).to.equal(0);
expect(@(system(detachInvocation.UTF8String))).to(equal(@0));
}];
return [NSURL fileURLWithPath:path isDirectory:YES];
}
- (BOOL)isRunningOnTravis {
return NSProcessInfo.processInfo.environment[@"TRAVIS"] != nil;
}
@end
#pragma clang diagnostic pop
@@ -6,9 +6,16 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLCodeSignature.h"
SpecBegin(SQRLCodeSignature)
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLCodeSignatureSpec)
__block NSBundle *bundle;
__block void (^corruptURL)(NSURL *URL);
@@ -17,26 +24,26 @@ beforeEach(^{
bundle = self.testApplicationBundle;
corruptURL = ^(NSURL *URL) {
expect([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL]).to.beTruthy();
expect(@([@"this bundle is corrupted, yo" writeToURL:bundle.executableURL atomically:YES encoding:NSUTF8StringEncoding error:NULL])).to(beTruthy());
};
});
it(@"should verify a valid bundle", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
});
it(@"should fail to verify with different code signing requirements", ^{
NSError *error = nil;
SQRLCodeSignature *verifier = [SQRLCodeSignature currentApplicationSignature:&error];
expect(verifier).notTo.beNil();
expect(error).to.beNil();
SQRLCodeSignature *signature = [SQRLCodeSignature currentApplicationSignature:&error];
expect(signature).notTo(beNil());
expect(error).to(beNil());
BOOL success = [[verifier verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(error).notTo.beNil();
BOOL success = [[signature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(error).notTo(beNil());
});
describe(@"code signature changes", ^{
@@ -47,15 +54,15 @@ describe(@"code signature changes", ^{
});
it(@"should fail to verify a bundle with a missing code signature", ^{
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
it(@"should fail to verify a bundle with a corrupt code signature", ^{
@@ -63,25 +70,25 @@ describe(@"code signature changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
});
describe(@"main executable changes", ^{
it(@"should fail to verify a bundle with a missing executable", ^{
expect([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL]).to.beTruthy();
expect(@([NSFileManager.defaultManager removeItemAtURL:bundle.executableURL error:NULL])).to(beTruthy());
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorCouldNotCreateStaticCode);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
it(@"should fail to verify a bundle with a corrupt executable", ^{
@@ -89,11 +96,11 @@ describe(@"main executable changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
});
@@ -102,19 +109,19 @@ describe(@"helper executable changes", ^{
beforeEach(^{
helperURL = [bundle URLForAuxiliaryExecutable:@"unused-helper"];
expect(helperURL).notTo.beNil();
expect(helperURL).notTo(beNil());
});
it(@"should fail to verify a bundle with a missing helper", ^{
expect([NSFileManager.defaultManager removeItemAtURL:helperURL error:NULL]).to.beTruthy();
expect(@([NSFileManager.defaultManager removeItemAtURL:helperURL error:NULL])).to(beTruthy());
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
it(@"should fail to verify a bundle with a corrupt helper", ^{
@@ -122,11 +129,11 @@ describe(@"helper executable changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
});
@@ -135,19 +142,19 @@ describe(@"resource changes", ^{
beforeEach(^{
resourceURL = [bundle URLForResource:@"MainMenu" withExtension:@"nib"];
expect(resourceURL).notTo.beNil();
expect(resourceURL).notTo(beNil());
});
it(@"should fail to verify a bundle with a missing resource", ^{
expect([NSFileManager.defaultManager removeItemAtURL:resourceURL error:NULL]).to.beTruthy();
expect(@([NSFileManager.defaultManager removeItemAtURL:resourceURL error:NULL])).to(beTruthy());
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
it(@"should fail to verify a bundle with a corrupt resource", ^{
@@ -155,11 +162,11 @@ describe(@"resource changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
});
@@ -171,15 +178,15 @@ describe(@"framework changes", ^{
});
it(@"should fail to verify a bundle with a missing framework", ^{
expect([NSFileManager.defaultManager removeItemAtURL:frameworkURL error:NULL]).to.beTruthy();
expect(@([NSFileManager.defaultManager removeItemAtURL:frameworkURL error:NULL])).to(beTruthy());
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
it(@"should fail to verify a bundle with a corrupt framework", ^{
@@ -187,12 +194,12 @@ describe(@"framework changes", ^{
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:bundle.bundleURL] waitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLCodeSignatureErrorDomain);
expect(error.code).to.equal(SQRLCodeSignatureErrorDidNotPass);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLCodeSignatureErrorDomain));
expect(@(error.code)).to(equal(@(SQRLCodeSignatureErrorDidNotPass)));
});
});
SpecEnd
QuickSpecEnd
-115
View File
@@ -1,115 +0,0 @@
//
// SQRLDeepCodesignSpec.m
// Squirrel
//
// Created by Keith Duncan on 2013-09-05.
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLDeepCodesign)
NSMutableDictionary * (^environmentSuitableForChildProcess)(void) = ^ {
// Remove environment variables that configure the Obj-C runtime
// Specifically OBJC_DISABLE_GC so that child processes aren't forced to
// adopt the GC preference of the test suite
NSMutableDictionary *environment = [NSProcessInfo.processInfo.environment mutableCopy];
NSSet *objcEnvironmentVariables = [environment keysOfEntriesPassingTest:^(NSString *variable, id obj, BOOL *stop) {
return [variable hasPrefix:@"OBJC"];
}];
[environment removeObjectsForKeys:objcEnvironmentVariables.allObjects];
return environment;
};
NSTask * (^codesignTaskWithArguments)(NSArray *) = ^ (NSArray *arguments) {
NSTask *task = [[NSTask alloc] init];
task.launchPath = @"/usr/bin/xcrun";
task.arguments = [@[ @"codesign" ] arrayByAddingObjectsFromArray:arguments];
task.environment = environmentSuitableForChildProcess();
return task;
};
void (^resignTestApplicationPreserveEverythingButTheRequirements)(void) = ^{
NSURL *testApplicationLocation = self.testApplicationURL;
NSTask *resignCodesignTask = codesignTaskWithArguments(@[
@"--sign", @"-",
@"--force",
@"--verbose=4",
@"--preserve-metadata=identifier,entitlements,resource-rules",
testApplicationLocation.path,
]);
[resignCodesignTask launch];
[resignCodesignTask waitUntilExit];
expect(resignCodesignTask.terminationStatus).to.equal(0);
};
void (^deepCodesignTestApplication)(void) = ^{
NSURL *testApplicationLocation = self.testApplicationURL;
NSBundle *testsBundle = [NSBundle bundleForClass:self.class];
NSURL *deepCodesignLocation = [testsBundle URLForResource:@"deep-codesign" withExtension:nil];
NSNumber *executable = nil;
NSError *executableError = nil;
BOOL getExecutable = [deepCodesignLocation getResourceValue:&executable forKey:NSURLIsExecutableKey error:&executableError];
expect(getExecutable).to.beTruthy();
expect(executable.boolValue).to.beTruthy();
expect(executableError).to.beNil();
NSTask *deepCodesignTask = [[NSTask alloc] init];
deepCodesignTask.launchPath = deepCodesignLocation.path;
NSMutableDictionary *environment = environmentSuitableForChildProcess();
[environment addEntriesFromDictionary:@{
@"CODE_SIGN_IDENTITY": @"-",
@"CONFIGURATION_BUILD_DIR": testApplicationLocation.URLByDeletingLastPathComponent.path,
@"FULL_PRODUCT_NAME": testApplicationLocation.lastPathComponent,
}];
deepCodesignTask.environment = environment;
[deepCodesignTask launch];
[deepCodesignTask waitUntilExit];
expect(deepCodesignTask.terminationStatus).to.equal(0);
/*
By signing test application's contents, which are covered by test
application's ResourceRules it's signature becomes invalid. Usually,
Xcode would sign the application after deep-codesign has run so the
signature would include the _signed_ child resources which are covered
by test application's ResourceRules
test application is already signed, so we need to manually resign it
deep-codesign preserves the requirements, we can't use it to resign the
root target, the new signing identity wouldn't verify against the
original designated requirement
*/
resignTestApplicationPreserveEverythingButTheRequirements();
};
BOOL (^deepVerify)(void) = ^ BOOL {
NSTask *deepVerifyTask = codesignTaskWithArguments(@[
@"--deep-verify",
@"--verbose=4",
self.testApplicationURL.path
]);
[deepVerifyTask launch];
[deepVerifyTask waitUntilExit];
return deepVerifyTask.terminationStatus == 0;
};
it(@"should deep sign the test application", ^{
deepCodesignTestApplication();
});
it(@"should deep verify after signing", ^{
expect(deepVerify()).to.beFalsy();
deepCodesignTestApplication();
expect(deepVerify()).to.beTruthy();
});
SpecEnd
+19 -14
View File
@@ -6,39 +6,44 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
SpecBegin(SQRLDirectoryManager)
QuickSpecBegin(SQRLDirectoryManagerSpec)
__block NSString *otestIdentifier;
beforeEach(^{
otestIdentifier = NSRunningApplication.currentApplication.localizedName;
expect(otestIdentifier).notTo.beNil();
otestIdentifier = NSProcessInfo.processInfo.environment[@"FORCE_APP_IDENTIFIER"];
expect(otestIdentifier).notTo(beNil());
});
it(@"should initialize", ^{
SQRLDirectoryManager *manager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier];
expect(manager).notTo.beNil();
expect(manager).notTo(beNil());
});
it(@"should create a manager for the current app", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
expect(manager).notTo.beNil();
expect(manager).to.equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]);
expect(manager).notTo(beNil());
expect(manager).to(equal([[SQRLDirectoryManager alloc] initWithApplicationIdentifier:otestIdentifier]));
});
it(@"should send an Application Support URL", ^{
SQRLDirectoryManager *manager = SQRLDirectoryManager.currentApplicationManager;
NSError *error = nil;
NSURL *appSupportURL = [[manager applicationSupportURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo.beNil();
expect(error).to.beNil();
NSURL *appSupportURL = [[manager storageURL] firstOrDefault:nil success:NULL error:&error];
expect(appSupportURL).notTo(beNil());
expect(error).to(beNil());
__block BOOL directory = NO;
expect([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory]).to.beTruthy();
expect(directory).to.beTruthy();
expect(@([NSFileManager.defaultManager fileExistsAtPath:appSupportURL.path isDirectory:&directory])).to(beTruthy());
expect(@(directory)).to(beTruthy());
});
it(@"should send a ShipIt state URL", ^{
@@ -46,8 +51,8 @@ it(@"should send a ShipIt state URL", ^{
NSError *error = nil;
NSURL *stateURL = [[manager shipItStateURL] firstOrDefault:nil success:NULL error:&error];
expect(stateURL).notTo.beNil();
expect(error).to.beNil();
expect(stateURL).notTo(beNil());
expect(error).to(beNil());
});
SpecEnd
QuickSpecEnd
+134 -72
View File
@@ -6,13 +6,32 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLCodeSignature.h"
#import "SQRLDirectoryManager.h"
#import "SQRLInstaller.h"
#import "SQRLShipItLauncher.h"
#import "SQRLShipItState.h"
#import "SQRLInstaller+Private.h"
#import "SQRLInstallerOwnedBundle.h"
#import "SQRLShipItRequest.h"
SpecBegin(SQRLInstaller)
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLInstallerSpec)
mode_t (^modeOfURL)(NSURL *) = ^ mode_t (NSURL *fileURL) {
NSFileSecurity *fileSecurity = nil;
BOOL success = [fileURL getResourceValue:&fileSecurity forKey:NSURLFileSecurityKey error:NULL];
expect(@(success)).to(beTruthy());
expect(fileSecurity).notTo(beNil());
__block mode_t mode;
expect(@(CFFileSecurityGetMode((__bridge CFFileSecurityRef)fileSecurity, &mode))).to(beTruthy());
return mode & (S_IRWXU | S_IRWXG | S_IRWXO);
};
__block NSURL *updateURL;
@@ -20,102 +39,148 @@ 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 useUpdateBundleName:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should install an update in process", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
[self installWithRequest:request remote:NO];
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should install an update and relaunch", ^{
NSString *bundleIdentifier = @"com.github.Squirrel.TestApplication";
NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier];
expect(apps.count).to.equal(0);
expect(@(apps.count)).to(equal(@0));
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 useUpdateBundleName:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count).will.equal(1);
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:bundleIdentifier].count)).toEventually(equal(@1));
});
it(@"should install an update from another volume", ^{
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication 2.1" fromDirectory:updateURL.URLByDeletingLastPathComponent];
updateURL = [diskImageURL URLByAppendingPathComponent:updateURL.lastPathComponent];
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 useUpdateBundleName:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should install an update to another volume", ^{
NSURL *diskImageURL = [self createAndMountDiskImageNamed:@"TestApplication" fromDirectory:self.testApplicationURL.URLByDeletingLastPathComponent];
NSURL *targetURL = [diskImageURL URLByAppendingPathComponent:self.testApplicationURL.lastPathComponent];
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 useUpdateBundleName:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
NSURL *plistURL = [targetURL URLByAppendingPathComponent:@"Contents/Info.plist"];
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).will.equal(SQRLTestApplicationUpdatedShortVersionString);
expect([NSDictionary dictionaryWithContentsOfURL:plistURL][SQRLBundleShortVersionStringKey]).withTimeout(SQRLLongTimeout).toEventually(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();
beforeEach(^{
targetURL = self.testApplicationURL;
NSError *installError = nil;
BOOL install = [[installer.installUpdateCommand execute:state] asynchronouslyWaitUntilCompleted:&installError];
expect(install).to.beTruthy();
expect(installError).to.beNil();
NSURL *copiedTargetURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication Target.app"];
expect(@([NSFileManager.defaultManager moveItemAtURL:targetURL toURL:copiedTargetURL error:NULL])).to(beTruthy());
SQRLCodeSignature *codeSignature = self.testApplicationSignature;
SQRLInstallerOwnedBundle *ownedBundle = [[SQRLInstallerOwnedBundle alloc] initWithOriginalURL:targetURL temporaryURL:copiedTargetURL codeSignature:codeSignature];
NSData *ownedBundleArchive = [NSKeyedArchiver archivedDataWithRootObject:ownedBundle];
expect(ownedBundleArchive).notTo(beNil());
// Set up ShipIt's preferences like it paused in the middle of an
// installation.
NSString *applicationIdentifier = self.shipItDirectoryManager.applicationIdentifier;
CFPreferencesSetValue((__bridge CFStringRef)SQRLShipItInstallationAttemptsKey, (__bridge CFPropertyListRef)@(4), (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
CFPreferencesSetValue((__bridge CFStringRef)SQRLInstallerOwnedBundleKey, (__bridge CFDataRef)ownedBundleArchive, (__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
BOOL synchronized = CFPreferencesSynchronize((__bridge CFStringRef)applicationIdentifier, kCFPreferencesCurrentUser, kCFPreferencesCurrentHost);
expect(@(synchronized)).to(beTruthy());
});
it(@"should not install an update after too many attempts", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
[self installWithRequest:request remote:YES];
__block NSError *error;
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).toEventually(beTruthy());
expect(error).to(beNil());
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
it(@"should relaunch even after failing to install an update", ^{
SQRLShipItRequest *request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:targetURL bundleIdentifier:nil launchAfterInstallation:YES useUpdateBundleName:NO];
[self installWithRequest:request remote:YES];
expect(@([NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.github.Squirrel.TestApplication"].count)).toEventually(equal(@1));
__block NSError *error;
expect(@([[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error])).to(beTruthy());
expect(error).to(beNil());
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
});
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 useUpdateBundleName: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).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect(@(modeOfURL(self.testApplicationURL))).to(equal(@0755));
expect(@(modeOfURL([self.testApplicationURL URLByAppendingPathComponent:@"Contents/MacOS/TestApplication"]))).to(equal(@0755));
});
describe(@"signal handling", ^{
__block NSURL *targetURL;
void (^verifyUpdate)(void) = ^{
// Wait up to the launchd throttle interval, then verify that ShipIt
// relaunched and finished installing the update.
expect(self.testApplicationBundleVersion).withTimeout(SQRLLongTimeout).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
NSError *error;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
};
beforeEach(^{
// Copied so that we don't recreate the TestApplication bundle by
// accessing the property.
targetURL = self.testApplicationURL;
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 useUpdateBundleName:NO];
[self launchShipIt];
[self installWithRequest:request remote:YES];
// Apply a random delay before sending the termination signal, to
// fuzz out race conditions.
@@ -123,45 +188,42 @@ describe(@"signal handling", ^{
[NSThread sleepForTimeInterval:delay];
});
afterEach(^{
// Wait up to the launchd throttle interval, then verify that ShipIt
// relaunched and finished installing the update.
Expecta.asynchronousTestTimeout = 5;
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
NSError *error = nil;
BOOL success = [[self.testApplicationSignature verifyBundleAtURL:targetURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
});
it(@"should handle SIGHUP", ^{
system("killall -v -HUP ShipIt");
system("killall -HUP ShipIt");
verifyUpdate();
});
it(@"should handle SIGTERM", ^{
system("killall -v -TERM ShipIt");
system("killall -TERM ShipIt");
verifyUpdate();
});
it(@"should handle SIGINT", ^{
system("killall -v -INT ShipIt");
system("killall -INT ShipIt");
verifyUpdate();
});
it(@"should handle SIGQUIT", ^{
system("killall -v -QUIT ShipIt");
system("killall -QUIT ShipIt");
verifyUpdate();
});
it(@"should handle SIGKILL", ^{
// SIGKILL is unique in that it'll always terminate ShipIt, so send it
// a few times to really test resumption.
for (int i = 0; i < 3; i++) {
system("killall -v -KILL ShipIt");
// a couple times to really test resumption.
//
// Two SIGKILL signals means that it'll be launched three times (which
// matches the maximum number of attempts per state).
for (int i = 0; i < 2; i++) {
system("killall -KILL ShipIt");
// Wait at least for the launchd throttle interval.
NSTimeInterval delay = 2 + (arc4random_uniform(100) / 1000.0);
[NSThread sleepForTimeInterval:delay];
}
verifyUpdate();
});
});
SpecEnd
QuickSpecEnd
+86
View File
@@ -0,0 +1,86 @@
//
// SQRLShipItStateSpec.m
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-10-09.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLShipItRequest.h"
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLShipItRequestSpec)
__block SQRLDirectoryManager *directoryManager;
__block SQRLShipItRequest *request;
beforeEach(^{
directoryManager = SQRLDirectoryManager.currentApplicationManager;
NSURL *updateURL = [self createTestApplicationUpdate];
request = [[SQRLShipItRequest alloc] initWithUpdateBundleURL:updateURL targetBundleURL:self.testApplicationURL bundleIdentifier:nil launchAfterInstallation:NO useUpdateBundleName:NO];
expect(request).notTo(beNil());
expect(request.targetBundleURL).to(equal(self.testApplicationURL));
expect(request.updateBundleURL).to(equal(updateURL));
expect(request.bundleIdentifier).to(beNil());
expect(@(request.launchAfterInstallation)).to(beFalsy());
});
afterEach(^{
NSURL *stateURL = [[directoryManager shipItStateURL] first];
expect(stateURL).notTo(beNil());
[NSFileManager.defaultManager removeItemAtURL:stateURL error:NULL];
});
it(@"should copy", ^{
SQRLShipItRequest *requestCopy = [request copy];
expect(requestCopy).to(equal(request));
expect(requestCopy).notTo(beIdenticalTo(request));
});
it(@"should fail to read state when no file exists yet", ^{
NSError *error;
BOOL success = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(@(success)).to(beFalsy());
expect(error).notTo(beNil());
});
it(@"should write and read state to disk", ^{
NSError *error;
BOOL success = [[request writeUsingURL:directoryManager.shipItStateURL] waitUntilCompleted:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
SQRLShipItRequest *readRequest = [[SQRLShipItRequest readUsingURL:directoryManager.shipItStateURL] firstOrDefault:nil success:&success error:&error];
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect(readRequest).to(equal(request));
});
it(@"should fail gracefully with archives encoding a different class", ^{
NSURL *archiveLocation = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"archive"];
NSError *error;
BOOL write = [[NSKeyedArchiver archivedDataWithRootObject:@"rogue object"] writeToURL:archiveLocation atomically:YES];
expect(@(write)).to(beTruthy());
expect(error).to(beNil());
BOOL success = NO;
SQRLShipItRequest *request = [[SQRLShipItRequest readUsingURL:[RACSignal return:archiveLocation]] firstOrDefault:nil success:&success error:&error];
expect(request).to(beNil());
expect(@(success)).to(beFalsy());
expect(error.domain).to(equal(SQRLShipItRequestErrorDomain));
expect(@(error.code)).to(equal(@(SQRLShipItRequestErrorUnarchiving)));
});
QuickSpecEnd
-63
View File
@@ -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
+27 -13
View File
@@ -6,15 +6,22 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLTerminationListener.h"
SpecBegin(SQRLTerminationListener)
#import "QuickSpec+SQRLFixtures.h"
QuickSpecBegin(SQRLTerminationListenerSpec)
__block SQRLTerminationListener *listener;
beforeEach(^{
listener = [[SQRLTerminationListener alloc] initWithURL:self.testApplicationURL bundleIdentifier:self.testApplicationBundle.bundleIdentifier];
expect(listener).notTo.beNil();
expect(listener).notTo(beNil());
});
it(@"should complete immediately when the app is not running", ^{
@@ -23,10 +30,12 @@ it(@"should complete immediately when the app is not running", ^{
completed = YES;
}];
expect(completed).to.beTruthy();
expect(@(completed)).to(beTruthy());
});
it(@"should wait until one instance terminates", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSRunningApplication *app = [self launchTestApplicationWithEnvironment:nil];
__block NSRunningApplication *observedApp = nil;
@@ -37,14 +46,17 @@ it(@"should wait until one instance terminates", ^{
completed = YES;
}];
expect(observedApp).will.equal(app);
expect(completed).to.beFalsy();
expect(observedApp).toEventually(equal(app));
expect(@(completed)).to(beFalsy());
expect([app terminate]).to.beTruthy();
expect(completed).will.beTruthy();
[app forceTerminate];
expect(@(app.terminated)).toEventually(beTruthy());
expect(@(completed)).toEventually(beTruthy());
});
it(@"should wait until multiple instances terminate", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSRunningApplication *app1 = [self launchTestApplicationWithEnvironment:nil];
NSRunningApplication *app2 = [self launchTestApplicationWithEnvironment:nil];
@@ -53,13 +65,15 @@ it(@"should wait until multiple instances terminate", ^{
completed = YES;
}];
expect(completed).to.beFalsy();
expect(@(completed)).to(beFalsy());
expect([app1 terminate]).to.beTruthy();
expect(completed).to.beFalsy();
[app1 forceTerminate];
expect(@(app1.terminated)).toEventually(beTruthy());
expect(@(completed)).to(beFalsy());
expect([app2 terminate]).to.beTruthy();
expect(completed).will.beTruthy();
[app2 forceTerminate];
expect(@(app2.terminated)).toEventually(beTruthy());
expect(@(completed)).toEventually(beTruthy());
});
SpecEnd
QuickSpecEnd
+19 -14
View File
@@ -6,22 +6,27 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
SpecBegin(SQRLUpdate)
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
QuickSpecBegin(SQRLUpdateSpec)
it(@"should return nil when initialised without a url", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{} error:&error];
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
});
it(@"should return nil when initialised with a url not in URL syntax", ^{
NSError *error = nil;
SQRLUpdate *update = [[SQRLUpdate alloc] initWithDictionary:@{ @"updateURL": [NSURL URLWithString:@"test"] } error:&error];
expect(update).to.beNil();
expect(error.domain).to.equal(NSCocoaErrorDomain);
expect(error.code).to.equal(NSKeyValueValidationError);
expect(update).to(beNil());
expect(error.domain).to(equal(NSCocoaErrorDomain));
expect(@(error.code)).to(equal(@(NSKeyValueValidationError)));
});
it(@"should validate release name and notes", ^{
@@ -32,20 +37,20 @@ it(@"should validate release name and notes", ^{
@"releaseNotes": [[NSObject alloc] init]
} error:NULL];
expect(update).notTo.beNil();
expect(update.updateURL).to.equal(updateURL);
expect(update.releaseName).to.beNil();
expect(update.releaseNotes).to.beNil();
expect(update).notTo(beNil());
expect(update.updateURL).to(equal(updateURL));
expect(update.releaseName).to(beNil());
expect(update.releaseNotes).to(beNil());
});
it(@"should parse Central style dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"Tue Sep 17 10:24:27 -0700 2013" } error:NULL];
expect(update.releaseDate).notTo.beNil();
expect(update.releaseDate).to(equal([NSDate dateWithTimeIntervalSince1970:1379438667]));
});
it(@"should parse ISO 8601 dates", ^{
SQRLUpdate *update = [MTLJSONAdapter modelOfClass:SQRLUpdate.class fromJSONDictionary:@{ @"url": @"http://example.com/update", @"pub_date": @"2013-09-18T13:17:07+01:00" } error:NULL];
expect(update.releaseDate).notTo.beNil();
expect(update.releaseDate).to(equal([NSDate dateWithTimeIntervalSince1970:1379506627]));
});
SpecEnd
QuickSpecEnd
+247 -73
View File
@@ -6,34 +6,44 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "SQRLTestUpdate.h"
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "SQRLDirectoryManager.h"
#import "SQRLZipArchiver.h"
SpecBegin(SQRLUpdater)
#import "OHHTTPStubs/OHHTTPStubs.h"
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLTestUpdate.h"
#import "TestAppConstants.h"
QuickSpecBegin(SQRLUpdaterSpec)
__block NSURL *JSONURL;
void (^writeUpdate)(SQRLUpdate *) = ^(SQRLUpdate *update) {
NSDictionary *updateInfo = [MTLJSONAdapter JSONDictionaryFromModel:update];
expect(updateInfo).notTo.beNil();
expect(updateInfo).notTo(beNil());
NSError *error = nil;
NSData *JSON = [NSJSONSerialization dataWithJSONObject:updateInfo options:NSJSONWritingPrettyPrinted error:&error];
expect(JSON).notTo.beNil();
expect(error).to.beNil();
expect(JSON).notTo(beNil());
expect(error).to(beNil());
BOOL success = [JSON writeToURL:JSONURL options:NSDataWritingAtomic error:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
};
NSURL * (^zipUpdate)(NSURL *) = ^(NSURL *updateURL) {
NSURL *zipFolderURL = [NSFileManager.defaultManager URLForDirectory:NSItemReplacementDirectory inDomain:NSUserDomainMask appropriateForURL:self.temporaryDirectoryURL create:YES error:NULL];
expect(zipFolderURL).notTo.beNil();
expect(zipFolderURL).notTo(beNil());
NSURL *zippedUpdateURL = [[zipFolderURL URLByAppendingPathComponent:updateURL.lastPathComponent] URLByAppendingPathExtension:@"zip"];
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zippedUpdateURL fromDirectoryAtURL:updateURL] asynchronouslyWaitUntilCompleted:NULL];
expect(success).to.beTruthy();
expect(@(success)).to(beTruthy());
return zippedUpdateURL;
};
@@ -49,85 +59,249 @@ beforeEach(^{
JSONURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"update.json"];
});
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
describe(@"updating", ^{
__block NSURL *updateURL;
writeUpdate(update);
beforeEach(^{
updateURL = [self createTestApplicationUpdate];
});
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
it(@"should use the application's bundled version of Squirrel and update in-place", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should not install a corrupt update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect(@([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL])).to(beTruthy());
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to(equal(SQRLTestApplicationOriginalShortVersionString));
});
it(@"should update to the most recently enqueued job", ^{
SKIP_IF_RUNNING_ON_TRAVIS
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(nil);
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
SKIP_IF_RUNNING_ON_TRAVIS
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSTimeInterval delay = 15;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
expect(@(app.terminated)).withTimeout(delay + SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
});
describe(@"cleaning up", ^{
__block NSURL *appSupportURL;
__block RACSignal *updateDirectoryURLs;
beforeEach(^{
SQRLDirectoryManager *directoryManager = [[SQRLDirectoryManager alloc] initWithApplicationIdentifier:@"com.github.Squirrel.TestApplication.ShipIt"];
appSupportURL = [[directoryManager storageURL] first];
expect(appSupportURL).notTo(beNil());
updateDirectoryURLs = [[RACSignal
defer:^{
NSArray *contents = [NSFileManager.defaultManager contentsOfDirectoryAtURL:appSupportURL includingPropertiesForKeys:nil options:0 error:NULL];
if (contents == nil) return [RACSignal empty];
return contents.rac_sequence.signal;
}]
filter:^(NSURL *directoryURL) {
return [directoryURL.lastPathComponent hasPrefix:@"update."];
}];
});
it(@"should remove downloaded archives after updating", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSError *error = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:&error];
expect(update).notTo(beNil());
expect(error).to(beNil());
writeUpdate(update);
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateRequestCount": @2 });
expect([updateDirectoryURLs toArray]).toEventuallyNot(equal(@[]));
expect(@(app.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
expect(self.testApplicationBundleVersion).toEventually(equal(SQRLTestApplicationUpdatedShortVersionString));
expect([updateDirectoryURLs toArray]).toEventually(equal(@[]));
});
});
});
it(@"should not install a corrupt update", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
NSURL *codeSignatureURL = [updateURL URLByAppendingPathComponent:@"Contents/_CodeSignature"];
expect([NSFileManager.defaultManager removeItemAtURL:codeSignatureURL error:NULL]).to.beTruthy();
describe(@"response handling", ^{
__block NSURLRequest *localRequest = nil;
__block SQRLUpdater *updater = nil;
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
beforeEach(^{
localRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"fake://host/path"]];
updater = [[SQRLUpdater alloc] initWithUpdateRequest:localRequest];
});
writeUpdate(update);
it(@"should return an error for non 2xx code HTTP responses", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:nil statusCode:/* Server Error */ 500 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
NSRunningApplication *app = launchWithEnvironment(nil);
expect(app.terminated).will.beTruthy();
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(@(result)).to(beFalsy());
expect(error.domain).to(equal(SQRLUpdaterErrorDomain));
expect(@(error.code)).to(equal(@(SQRLUpdaterErrorInvalidServerResponse)));
});
// Give the update some time to finish installing.
[NSThread sleepForTimeInterval:0.2];
expect(self.testApplicationBundleVersion).to.equal(SQRLTestApplicationOriginalShortVersionString);
it(@"should return an error for non JSON data", ^{
OHHTTPStubs *stubs = [OHHTTPStubs shouldStubRequestsPassingTest:^(NSURLRequest *request) {
return [request.URL isEqual:localRequest.URL];
} withStubResponse:^(NSURLRequest *request) {
return [OHHTTPStubsResponse responseWithData:NSData.data statusCode:/* OK */ 200 responseTime:0 headers:nil];
}];
[self addCleanupBlock:^{
[OHHTTPStubs removeRequestHandler:stubs];
}];
NSError *error = nil;
BOOL result = [[updater.checkForUpdatesCommand execute:nil] asynchronouslyWaitUntilCompleted:&error];
expect(@(result)).to(beFalsy());
expect(error.domain).to(equal(SQRLUpdaterErrorDomain));
expect(@(error.code)).to(equal(@(SQRLUpdaterErrorInvalidServerBody)));
});
});
it(@"should update to the most recently enqueued job", ^{
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(self.testApplicationURL)
} error:NULL];
static RACSignal * (^stateNotificationListener)(void) = ^ {
return [[[NSDistributedNotificationCenter.defaultCenter
rac_addObserverForName:SQRLTestAppUpdaterStateTransitionNotificationName object:nil]
map:^(NSNotification *notification) {
return notification.userInfo[SQRLTestAppUpdaterStateKey];
}]
setNameWithFormat:@"stateNotificationListener"];
};
writeUpdate(update);
describe(@"state", ^{
it(@"should transition through idle, checking and idle, when there is no update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
NSRunningApplication *app = launchWithEnvironment(nil);
NSMutableArray *states = [NSMutableArray array];
[stateNotificationListener() subscribeNext:^(NSNumber *state) {
[states addObject:state];
}];
// Now that Test Application is launched, it's going to keep checking the
// JSON URL until it has the proper release name. So we'll wait a short bit,
// and then add the correct name in.
//
// This exercises ShipIt's ability to discard previous commands and
// install an even newer update.
[NSThread sleepForTimeInterval:0.3];
NSRunningApplication *testApplication = launchWithEnvironment(nil);
NSURL *updateURL = [self createTestApplicationUpdate];
update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
NSArray *expectedStates = @[
@(SQRLUpdaterStateIdle),
@(SQRLUpdaterStateCheckingForUpdate),
@(SQRLUpdaterStateIdle),
];
expect(states).toEventually(equal(expectedStates));
writeUpdate(update);
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
});
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
it(@"should transition through idle, checking, downloading and awaiting relaunch, when there is an update", ^{
SKIP_IF_RUNNING_ON_TRAVIS
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).withTimeout(SQRLLongTimeout).toEventually(equal(expectedStates));
expect(@(testApplication.terminated)).withTimeout(SQRLLongTimeout).toEventually(beTruthy());
});
});
it(@"should use the application's bundled version of Squirrel and update in-place after a significant delay", ^{
NSURL *updateURL = [self createTestApplicationUpdate];
SQRLTestUpdate *update = [SQRLTestUpdate modelWithDictionary:@{
@"updateURL": zipUpdate(updateURL),
@"final": @YES
} error:NULL];
writeUpdate(update);
NSTimeInterval delay = 30;
NSRunningApplication *app = launchWithEnvironment(@{ @"SQRLUpdateDelay": [NSString stringWithFormat:@"%f", delay] });
Expecta.asynchronousTestTimeout = delay + 3;
expect(app.terminated).will.beTruthy();
expect(self.testApplicationBundleVersion).will.equal(SQRLTestApplicationUpdatedShortVersionString);
});
SpecEnd
QuickSpecEnd
+26 -20
View File
@@ -6,38 +6,44 @@
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Nimble/Nimble.h>
#import <Quick/Quick.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <Squirrel/Squirrel.h>
#import "QuickSpec+SQRLFixtures.h"
#import "SQRLCodeSignature.h"
#import "SQRLZipArchiver.h"
SpecBegin(SQRLZipArchiver)
QuickSpecBegin(SQRLZipArchiverSpec)
it(@"should extract a zip archive created by the Finder", ^{
NSURL *zipURL = [[NSBundle bundleForClass:self.class] URLForResource:@"TestApplication.app" withExtension:@"zip"];
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication 2.1.app"];
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
});
it(@"should fail to extract a nonexistent zip archive", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver unzipArchiveAtURL:[self.temporaryDirectoryURL URLByAppendingPathComponent:@"foo.zip"] intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(success).to.beFalsy();
expect(@(success)).to(beFalsy());
NSLog(@"%@", error);
expect(error).notTo.beNil();
expect(error.domain).to.equal(SQRLZipArchiverErrorDomain);
expect(error.code).to.equal(SQRLZipArchiverShellTaskFailed);
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo.equal(0);
expect(error).notTo(beNil());
expect(error.domain).to(equal(SQRLZipArchiverErrorDomain));
expect(@(error.code)).to(equal(@(SQRLZipArchiverShellTaskFailed)));
expect(error.userInfo[SQRLZipArchiverExitCodeErrorKey]).notTo(equal(0));
});
it(@"should create a zip archive readable by itself", ^{
@@ -45,21 +51,21 @@ it(@"should create a zip archive readable by itself", ^{
NSError *error = nil;
BOOL success = [[SQRLZipArchiver createZipArchiveAtURL:zipURL fromDirectoryAtURL:self.testApplicationURL] asynchronouslyWaitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
expect([NSFileManager.defaultManager fileExistsAtPath:zipURL.path]).to.beTruthy();
expect(@([NSFileManager.defaultManager fileExistsAtPath:zipURL.path])).to(beTruthy());
success = [[SQRLZipArchiver unzipArchiveAtURL:zipURL intoDirectoryAtURL:self.temporaryDirectoryURL] asynchronouslyWaitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
NSURL *extractedAppURL = [self.temporaryDirectoryURL URLByAppendingPathComponent:@"TestApplication.app"];
expect([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path]).to.beTruthy();
expect(@([NSFileManager.defaultManager fileExistsAtPath:extractedAppURL.path])).to(beTruthy());
success = [[self.testApplicationSignature verifyBundleAtURL:extractedAppURL] waitUntilCompleted:&error];
expect(success).to.beTruthy();
expect(error).to.beNil();
expect(@(success)).to(beTruthy());
expect(error).to(beNil());
});
SpecEnd
QuickSpecEnd
+1 -1
View File
@@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>${EXECUTABLE_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.github.${PRODUCT_NAME:rfc1034identifier}</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
-25
View File
@@ -1,25 +0,0 @@
//
// SquirrelTests-Prefix.pch
// Squirrel
//
// Created by Justin Spahr-Summers on 2013-07-21.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#ifdef __OBJC__
#import <Cocoa/Cocoa.h>
#import <SenTestingKit/SenTestingKit.h>
#import <Mantle/Mantle.h>
#import <ReactiveCocoa/ReactiveCocoa.h>
#import <ReactiveCocoa/EXTScope.h>
#import <Squirrel/Squirrel.h>
#define SPT_SUBCLASS SQRLTestCase
#import "Specta.h"
#define EXP_SHORTHAND
#import "Expecta.h"
#import "SQRLTestCase.h"
#endif
+19
View File
@@ -0,0 +1,19 @@
//
// SwiftSpec.swift
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2014-10-14.
// Copyright (c) 2014 GitHub. All rights reserved.
//
import Foundation
import Nimble
import Quick
// Without this, the Swift stdlib won't be linked into the test target (even if
// “Embedded Content Contains Swift Code” is enabled).
class SwiftSpec: QuickSpec {
override func spec() {
expect(true).to(beTruthy())
}
}
@@ -0,0 +1 @@
Versions/Current/Headers
@@ -0,0 +1 @@
Versions/Current/Mantle
@@ -0,0 +1 @@
Versions/Current/Modules
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,172 @@
//
// MTLJSONAdapter.h
// Mantle
//
// Created by Justin Spahr-Summers on 2013-02-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class MTLModel;
// A MTLModel object that supports being parsed from and serialized to JSON.
@protocol MTLJSONSerializing
@required
// Specifies how to map property keys to different key paths in JSON.
//
// Subclasses overriding this method should combine their values with those of
// `super`.
//
// Any property keys not present in the dictionary are assumed to match the JSON
// key that should be used. Any keys associated with NSNull will not participate
// in JSON serialization.
//
// Returns a dictionary mapping property keys to JSON key paths (as strings) or
// NSNull values.
+ (NSDictionary *)JSONKeyPathsByPropertyKey;
@optional
// Specifies how to convert a JSON value to the given property key. If
// reversible, the transformer will also be used to convert the property value
// back to JSON.
//
// If the receiver implements a `+<key>JSONTransformer` method, MTLJSONAdapter
// will use the result of that method instead.
//
// Returns a value transformer, or nil if no transformation should be performed.
+ (NSValueTransformer *)JSONTransformerForKey:(NSString *)key;
// Overridden to parse the receiver as a different class, based on information
// in the provided dictionary.
//
// This is mostly useful for class clusters, where the abstract base class would
// be passed into -[MTLJSONAdapter initWithJSONDictionary:modelClass:], but
// a subclass should be instantiated instead.
//
// JSONDictionary - The JSON dictionary that will be parsed.
//
// Returns the class that should be parsed (which may be the receiver), or nil
// to abort parsing (e.g., if the data is invalid).
+ (Class)classForParsingJSONDictionary:(NSDictionary *)JSONDictionary;
@end
// The domain for errors originating from MTLJSONAdapter.
extern NSString * const MTLJSONAdapterErrorDomain;
// +classForParsingJSONDictionary: returned nil for the given dictionary.
extern const NSInteger MTLJSONAdapterErrorNoClassFound;
// The provided JSONDictionary is not valid.
extern const NSInteger MTLJSONAdapterErrorInvalidJSONDictionary;
// The model's implementation of +JSONKeyPathsByPropertyKey included a key which
// does not actually exist in +propertyKeys.
extern const NSInteger MTLJSONAdapterErrorInvalidJSONMapping;
// Converts a MTLModel object to and from a JSON dictionary.
@interface MTLJSONAdapter : NSObject
// The model object that the receiver was initialized with, or that the receiver
// parsed from a JSON dictionary.
@property (nonatomic, strong, readonly) MTLModel<MTLJSONSerializing> *model;
// Attempts to parse a JSON dictionary into a model object.
//
// modelClass - The MTLModel subclass to attempt to parse from the JSON.
// This class must conform to <MTLJSONSerializing>. This
// argument must not be nil.
// JSONDictionary - A dictionary representing JSON data. This should match the
// format returned by NSJSONSerialization. If this argument is
// nil, the method returns nil.
// error - If not NULL, this may be set to an error that occurs during
// parsing or initializing an instance of `modelClass`.
//
// Returns an instance of `modelClass` upon success, or nil if a parsing error
// occurred.
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary error:(NSError **)error;
// Attempts to parse an array of JSON dictionary objects into a model objects
// of a specific class.
//
// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
// class must conform to <MTLJSONSerializing>. This argument must
// not be nil.
// JSONArray - A array of dictionaries representing JSON data. This should
// match the format returned by NSJSONSerialization. If this
// argument is nil, the method returns nil.
// error - If not NULL, this may be set to an error that occurs during
// parsing or initializing an any of the instances of
// `modelClass`.
//
// Returns an array of `modelClass` instances upon success, or nil if a parsing
// error occurred.
+ (NSArray *)modelsOfClass:(Class)modelClass fromJSONArray:(NSArray *)JSONArray error:(NSError **)error;
// Converts a model into a JSON representation.
//
// model - The model to use for JSON serialization. This argument must not be
// nil.
//
// Returns a JSON dictionary, or nil if a serialization error occurred.
+ (NSDictionary *)JSONDictionaryFromModel:(MTLModel<MTLJSONSerializing> *)model;
// Converts a array of models into a JSON representation.
//
// models - The array of models to use for JSON serialization. This argument
// must not be nil.
//
// Returns a JSON array, or nil if a serialization error occurred for any
// model.
+ (NSArray *)JSONArrayFromModels:(NSArray *)models;
// Initializes the receiver by attempting to parse a JSON dictionary into
// a model object.
//
// JSONDictionary - A dictionary representing JSON data. This should match the
// format returned by NSJSONSerialization. If this argument is
// nil, the method returns nil and an error with code
// MTLJSONAdapterErrorInvalidJSONDictionary.
// modelClass - The MTLModel subclass to attempt to parse from the JSON.
// This class must conform to <MTLJSONSerializing>. This
// argument must not be nil.
// error - If not NULL, this may be set to an error that occurs during
// parsing or initializing an instance of `modelClass`.
//
// Returns an initialized adapter upon success, or nil if a parsing error
// occurred.
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass error:(NSError **)error;
// Initializes the receiver with an existing model.
//
// model - The model to use for JSON serialization. This argument must not be
// nil.
- (id)initWithModel:(MTLModel<MTLJSONSerializing> *)model;
// Serializes the receiver's `model` into JSON.
//
// Returns a JSON dictionary, or nil if a serialization error occurred.
- (NSDictionary *)JSONDictionary;
// Looks up the JSON key path in the model's +propertyKeys.
//
// Subclasses may override this method to customize the adapter's seralizing
// behavior. You should not call this method directly.
//
// key - The property key to retrieve the corresponding JSON key path for. This
// argument must not be nil.
//
// Returns a key path to use, or nil to omit the property from JSON.
- (NSString *)JSONKeyPathForPropertyKey:(NSString *)key;
@end
@interface MTLJSONAdapter (Deprecated)
+ (id)modelOfClass:(Class)modelClass fromJSONDictionary:(NSDictionary *)JSONDictionary __attribute__((deprecated("Replaced by +modelOfClass:fromJSONDictionary:error:")));
- (id)initWithJSONDictionary:(NSDictionary *)JSONDictionary modelClass:(Class)modelClass __attribute__((deprecated("Replaced by -initWithJSONDictionary:modelClass:error:")));
@end
@@ -0,0 +1,215 @@
//
// MTLManagedObjectAdapter.h
// Mantle
//
// Created by Justin Spahr-Summers on 2013-03-29.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <CoreData/CoreData.h>
@class MTLModel;
// A MTLModel object that supports being serialized to and from Core Data as an
// NSManagedObject.
@protocol MTLManagedObjectSerializing
@required
// The name of the Core Data entity that the receiver serializes to and
// deserializes from.
//
// This method must not return nil.
+ (NSString *)managedObjectEntityName;
// Specifies how to map property keys to different keys on the receiver's
// +managedObjectEntity.
//
// Entity attributes will be mapped to and from the receiver's properties using
// +entityAttributeTransformerForKey:. Entity relationships will be mapped to
// and from MTLModel objects using +relationshipModelClassesByPropertyKey.
// Fetched properties are not supported.
//
// Subclasses overriding this method should combine their values with those of
// `super`.
//
// Any property keys not present in the dictionary are assumed to match the
// entity key that should be used. Any keys associated with NSNull will not
// participate in managed object serialization.
//
// Returns a dictionary mapping property keys to entity keys (as strings) or
// NSNull values.
+ (NSDictionary *)managedObjectKeysByPropertyKey;
@optional
// Specifies a set of property keys used by the adapter to check for an already
// existing managed object when converting the MTLModel to its related
// NSManagedObject.
//
// The adapter will first map any keys provided by this method to the correct
// keys in managedObjectKeysByPropertyKey.
//
// The adapter will then perform a fetch request in the provided context for a
// managed object that matches the MTLModel's managedObjectEntityName and has
// equal values set for the property keys on the MTLModel.
//
// The managed object returned by the fetch request will then be set with all
// values from the MTLModel that the managed object is being converted from.
//
// If a property value of our MTLModel is yet another MTLModel which needs to be
// converted to a managed object, the class for that MTLModel can also implement
// this method to perform its own uniqing.
//
// For example:
// 1. An MTLModel subclass has id_number = 10.
// 2. An NSManagedObject accessible to the adapter's context has idnumber = 10.
// 3. managedObjectKeysByPropertyKey returns @{@"id_number" : @"idnumber"}
// 4. propertyKeysForManagedObjectUniquing returns
// [NSSet setWithObject:@"id_number"];
// 5. Then our fetch request may return this managed object (or another managed
// object with idnumber = 10).
//
// NOTE: If multiple managed objects follow the same uniquing criteria only one
// of them will be set with our MTLModel's values.
+ (NSSet *)propertyKeysForManagedObjectUniquing;
// Specifies how to convert the given property key to a managed object
// attribute. If reversible, the transformer will also be used to convert the
// managed object attribute back to the property.
//
// If the receiver implements a `+<key>EntityAttributeTransformer` method,
// MTLManagedObjectAdapter will use the result of that method instead.
//
// Returns a value transformer, or nil if no transformation should be performed.
+ (NSValueTransformer *)entityAttributeTransformerForKey:(NSString *)key;
// Specifies the MTLModel subclasses that should be deserialized to the
// receiver's property keys when a property key corresponds to an entity
// relationship.
//
// In other words, the dictionary returned by this method is used to decode
// managed object relationships into MTLModels (or NSArrays or NSSets thereof)
// set on the receiver.
//
// If a property key is omitted from the returned dictionary, but present in
// +managedObjectKeysByPropertyKey, and the receiver's +managedObjectEntity has
// a relationship at the corresponding key, an exception will be thrown during
// deserialization.
//
// Subclasses overriding this method should combine their values with those of
// `super`.
//
// Returns a dictionary mapping property keys to the Class objects that should
// be used.
+ (NSDictionary *)relationshipModelClassesByPropertyKey;
// Overridden to deserialize a different class instead of the receiver, based on
// information in the provided object.
//
// This is mostly useful for class clusters, where the abstract base class would
// be passed into +[MTLManagedObjectAdapter
// modelOfClass:fromManagedObject:error:], but a subclass should be instantiated
// instead.
//
// managedObject - The object that will be deserialized.
//
// Returns the class that should be instantiated (which may be the receiver), or
// nil to abort parsing (e.g., if the data is invalid).
+ (Class)classForDeserializingManagedObject:(NSManagedObject *)managedObject;
// Overriden when merging the value of the given key on the receiver with the
// value of the same key from the given `NSManagedObject` requires custom
// handling.
//
// By default, this method is not implemented, and precedence will be given to
// the value of the receiving model implicitly.
//
// When implemented, this method is called when an existing `NSManagedObject`
// is found for the receiving model, before updating the `NSManagedObject`'s
// properties.
//
// When implementing, you should use `+managedObjectKeysByPropertyKey` to map
// the given `key` to the appropriate `NSManagedObject` property.
- (void)mergeValueForKey:(NSString *)key fromManagedObject:(NSManagedObject *)managedObject;
// Overriden when merging values on the receiver with the given
// `NSManagedObject` requires custom handling.
//
// By default, this method is not implemented, and precedence will be given to
// the values of the receiving model implicitly.
//
// When implemented, this method is called when an existing `NSManagedObject`
// is found for the receiving model, before updating the `NSManagedObject`'s
// properties.
//
// When implementing, you should use `+managedObjectKeysByPropertyKey` to map
// the given `key` to the appropriate `NSManagedObject` property.
//
// If you have also implemented `mergeValueForKey:fromManagedObject:` you have
// to make sure to call `mergeValueForKey:fromManagedObject:` from this method
// when appropriate.
- (void)mergeValuesForKeysFromManagedObject:(NSManagedObject *)managedObject;
@end
// The domain for errors originating from MTLManagedObjectAdapter.
extern NSString * const MTLManagedObjectAdapterErrorDomain;
// +classForDeserializingManagedObject: returned nil for the given object.
extern const NSInteger MTLManagedObjectAdapterErrorNoClassFound;
// An NSManagedObject failed to initialize.
extern const NSInteger MTLManagedObjectAdapterErrorInitializationFailed;
// The managed object key specified by +managedObjectKeysByPropertyKey does not
// exist in the NSEntityDescription.
extern const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectKey;
// The managed object property specified has a type that isn't supported by
// MTLManagedObjectAdapter.
extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedManagedObjectPropertyType;
// The fetch request to find an existing managed object based on
// `+propertyKeysForManagedObjectUniquing` failed.
extern const NSInteger MTLManagedObjectAdapterErrorUniqueFetchRequestFailed;
// A MTLModel property cannot be serialized to or deserialized from an
// NSManagedObject relationship.
//
// For a to-one relationship, this means that the property does not contain
// a MTLModel, or the MTLModel does not conform to <MTLManagedObjectSerializing>.
//
// For a to-many relationship, this means that the property does not contain an
// NSArray or NSSet of MTLModel<MTLManagedObjectSerializing> instances.
extern const NSInteger MTLManagedObjectAdapterErrorUnsupportedRelationshipClass;
// The model's implementation of +managedObjectKeysByPropertyKey included a key
// which does not actually exist in +propertyKeys.
extern const NSInteger MTLManagedObjectAdapterErrorInvalidManagedObjectMapping;
// Converts a MTLModel object to and from an NSManagedObject.
@interface MTLManagedObjectAdapter : NSObject
// Attempts to deserialize an NSManagedObject into a MTLModel object.
//
// modelClass - The MTLModel subclass to return. This class must conform to
// <MTLManagedObjectSerializing>. This argument must not be nil.
// managedObject - The managed object to deserialize. If this argument is nil,
// the method returns nil.
// error - If not NULL, this may be set to an error that occurs during
// deserialization or initializing an instance of `modelClass`.
//
// Returns an instance of `modelClass` upon success, or nil if an error
// occurred.
+ (id)modelOfClass:(Class)modelClass fromManagedObject:(NSManagedObject *)managedObject error:(NSError **)error;
// Serializes a MTLModel into an NSManagedObject.
//
// model - The model object to serialize. This argument must not be nil.
// context - The context into which to insert the created managed object. This
// argument must not be nil.
// error - If not NULL, this may be set to an error that occurs during
// serialization or insertion.
+ (id)managedObjectFromModel:(MTLModel<MTLManagedObjectSerializing> *)model insertingIntoContext:(NSManagedObjectContext *)context error:(NSError **)error;
@end
@@ -0,0 +1,128 @@
//
// MTLModel+NSCoding.h
// Mantle
//
// Created by Justin Spahr-Summers on 2013-02-12.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import "MTLModel.h"
// Defines how a MTLModel property key should be encoded into an archive.
//
// MTLModelEncodingBehaviorExcluded - The property should never be encoded.
// MTLModelEncodingBehaviorUnconditional - The property should always be
// encoded.
// MTLModelEncodingBehaviorConditional - The object should be encoded only
// if unconditionally encoded elsewhere.
// This should only be used for object
// properties.
typedef enum : NSUInteger {
MTLModelEncodingBehaviorExcluded = 0,
MTLModelEncodingBehaviorUnconditional,
MTLModelEncodingBehaviorConditional,
} MTLModelEncodingBehavior;
// Implements default archiving and unarchiving behaviors for MTLModel.
@interface MTLModel (NSCoding) <NSCoding>
// Initializes the receiver from an archive.
//
// This will decode the original +modelVersion of the archived object, then
// invoke -decodeValueForKey:withCoder:modelVersion: for each of the receiver's
// +propertyKeys.
//
// Returns an initialized model object, or nil if a decoding error occurred.
- (id)initWithCoder:(NSCoder *)coder;
// Archives the receiver using the given coder.
//
// This will encode the receiver's +modelVersion, then the receiver's properties
// according to the behaviors specified in +encodingBehaviorsByPropertyKey.
- (void)encodeWithCoder:(NSCoder *)coder;
// Determines how the +propertyKeys of the class are encoded into an archive.
// The values of this dictionary should be boxed MTLModelEncodingBehavior
// values.
//
// Any keys not present in the dictionary will be excluded from the archive.
//
// Subclasses overriding this method should combine their values with those of
// `super`.
//
// Returns a dictionary mapping the receiver's +propertyKeys to default encoding
// behaviors. If a property is an object with `weak` semantics, the default
// behavior is MTLModelEncodingBehaviorConditional; otherwise, the default is
// MTLModelEncodingBehaviorUnconditional.
+ (NSDictionary *)encodingBehaviorsByPropertyKey;
// Determines the classes that are allowed to be decoded for each of the
// receiver's properties when using <NSSecureCoding>. The values of this
// dictionary should be NSArrays of Class objects.
//
// If any encodable keys (as determined by +encodingBehaviorsByPropertyKey) are
// not present in the dictionary, an exception will be thrown during secure
// encoding or decoding.
//
// Subclasses overriding this method should combine their values with those of
// `super`.
//
// Returns a dictionary mapping the receiver's encodable keys (as determined by
// +encodingBehaviorsByPropertyKey) to default allowed classes, based on the
// type that each property is declared as. If type of an encodable property
// cannot be determined (e.g., it is declared as `id`), it will be omitted from
// the dictionary, and subclasses must provide a valid value to prevent an
// exception being thrown during encoding/decoding.
+ (NSDictionary *)allowedSecureCodingClassesByPropertyKey;
// Decodes the value of the given property key from an archive.
//
// By default, this method looks for a `-decode<Key>WithCoder:modelVersion:`
// method on the receiver, and invokes it if found.
//
// If the custom method is not implemented and `coder` does not require secure
// coding, `-[NSCoder decodeObjectForKey:]` will be invoked with the given
// `key`.
//
// If the custom method is not implemented and `coder` requires secure coding,
// `-[NSCoder decodeObjectOfClasses:forKey:]` will be invoked with the
// information from +allowedSecureCodingClassesByPropertyKey and the given `key`. The
// receiver must conform to <NSSecureCoding> for this to work correctly.
//
// key - The property key to decode the value for. This argument cannot
// be nil.
// coder - The NSCoder representing the archive being decoded. This
// argument cannot be nil.
// modelVersion - The version of the original model object that was encoded.
//
// Returns the decoded and boxed value, or nil if the key was not present.
- (id)decodeValueForKey:(NSString *)key withCoder:(NSCoder *)coder modelVersion:(NSUInteger)modelVersion;
// The version of this MTLModel subclass.
//
// This version number is saved in archives so that later model changes can be
// made backwards-compatible with old versions.
//
// Subclasses should override this method to return a higher version number
// whenever a breaking change is made to the model.
//
// Returns 0.
+ (NSUInteger)modelVersion;
@end
// This method must be overridden to support archives created by older versions
// of Mantle (before the `MTLModel+NSCoding` interface existed).
@interface MTLModel (OldArchiveSupport)
// Converts an archived external representation to a dictionary suitable for
// passing to -initWithDictionary:.
//
// externalRepresentation - The decoded external representation of the receiver.
// fromVersion - The model version at the time the external
// representation was encoded.
//
// Returns nil by default, indicating that conversion failed.
+ (NSDictionary *)dictionaryValueFromArchivedExternalRepresentation:(NSDictionary *)externalRepresentation version:(NSUInteger)fromVersion;
@end
@@ -0,0 +1,125 @@
//
// MTLModel.h
// Mantle
//
// Created by Justin Spahr-Summers on 2012-09-11.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
// An abstract base class for model objects, using reflection to provide
// sensible default behaviors.
//
// The default implementations of <NSCopying>, -hash, and -isEqual: make use of
// the +propertyKeys method.
@interface MTLModel : NSObject <NSCopying>
// Returns a new instance of the receiver initialized using
// -initWithDictionary:error:.
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
// Initializes the receiver with default values.
//
// This is the designated initializer for this class.
- (instancetype)init;
// Initializes the receiver using key-value coding, setting the keys and values
// in the given dictionary.
//
// Subclass implementations may override this method, calling the super
// implementation, in order to perform further processing and initialization
// after deserialization.
//
// dictionaryValue - Property keys and values to set on the receiver. Any NSNull
// values will be converted to nil before being used. KVC
// validation methods will automatically be invoked for all of
// the properties given. If nil, this method is equivalent to
// -init.
// error - If not NULL, this may be set to any error that occurs
// (like a KVC validation error).
//
// Returns an initialized model object, or nil if validation failed.
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue error:(NSError **)error;
// Returns the keys for all @property declarations, except for `readonly`
// properties without ivars, or properties on MTLModel itself.
+ (NSSet *)propertyKeys;
// A dictionary representing the properties of the receiver.
//
// The default implementation combines the values corresponding to all
// +propertyKeys into a dictionary, with any nil values represented by NSNull.
//
// This property must never be nil.
@property (nonatomic, copy, readonly) NSDictionary *dictionaryValue;
// Merges the value of the given key on the receiver with the value of the same
// key from the given model object, giving precedence to the other model object.
//
// By default, this method looks for a `-merge<Key>FromModel:` method on the
// receiver, and invokes it if found. If not found, and `model` is not nil, the
// value for the given key is taken from `model`.
- (void)mergeValueForKey:(NSString *)key fromModel:(MTLModel *)model;
// Merges the values of the given model object into the receiver, using
// -mergeValueForKey:fromModel: for each key in +propertyKeys.
//
// `model` must be an instance of the receiver's class or a subclass thereof.
- (void)mergeValuesForKeysFromModel:(MTLModel *)model;
// Compares the receiver with another object for equality.
//
// The default implementation is equivalent to comparing both models'
// -dictionaryValue.
//
// Note that this may lead to infinite loops if the receiver holds a circular
// reference to another MTLModel and both use the default behavior.
// It is recommended to override -isEqual: in this scenario.
- (BOOL)isEqual:(id)object;
// A string that describes the contents of the receiver.
//
// The default implementation is based on the receiver's class and its
// -dictionaryValue.
//
// Note that this may lead to infinite loops if the receiver holds a circular
// reference to another MTLModel and both use the default behavior.
// It is recommended to override -description in this scenario.
- (NSString *)description;
@end
// Implements validation logic for MTLModel.
@interface MTLModel (Validation)
// Validates the model.
//
// The default implementation simply invokes -validateValue:forKey:error: with
// all +propertyKeys and their current value. If -validateValue:forKey:error:
// returns a new value, the property is set to that new value.
//
// error - If not NULL, this may be set to any error that occurs during
// validation
//
// Returns YES if the model is valid, or NO if the validation failed.
- (BOOL)validate:(NSError **)error;
@end
@interface MTLModel (Unavailable)
+ (instancetype)modelWithDictionary:(NSDictionary *)dictionaryValue __attribute__((deprecated("Replaced by +modelWithDictionary:error:")));
- (instancetype)initWithDictionary:(NSDictionary *)dictionaryValue __attribute__((deprecated("Replaced by -initWithDictionary:error:")));
+ (instancetype)modelWithExternalRepresentation:(NSDictionary *)externalRepresentation __attribute__((deprecated("Replaced by -[MTLJSONAdapter initWithJSONDictionary:modelClass:]")));
- (instancetype)initWithExternalRepresentation:(NSDictionary *)externalRepresentation __attribute__((deprecated("Replaced by -[MTLJSONAdapter initWithJSONDictionary:modelClass:]")));
@property (nonatomic, copy, readonly) NSDictionary *externalRepresentation __attribute__((deprecated("Replaced by MTLJSONAdapter.JSONDictionary")));
+ (NSDictionary *)externalRepresentationKeyPathsByPropertyKey __attribute__((deprecated("Replaced by +JSONKeyPathsByPropertyKey in <MTLJSONSerializing>")));
+ (NSValueTransformer *)transformerForKey:(NSString *)key __attribute__((deprecated("Replaced by +JSONTransformerForKey: in <MTLJSONSerializing>")));
+ (NSDictionary *)migrateExternalRepresentation:(NSDictionary *)externalRepresentation fromVersion:(NSUInteger)fromVersion __attribute__((deprecated("Replaced by -decodeValueForKey:withCoder:modelVersion:")));
@end
@@ -0,0 +1,29 @@
//
// MTLValueTransformer.h
// Mantle
//
// Created by Justin Spahr-Summers on 2012-09-11.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef id (^MTLValueTransformerBlock)(id);
//
// A value transformer supporting block-based transformation.
//
@interface MTLValueTransformer : NSValueTransformer
// Returns a transformer which transforms values using the given block. Reverse
// transformations will not be allowed.
+ (instancetype)transformerWithBlock:(MTLValueTransformerBlock)transformationBlock;
// Returns a transformer which transforms values using the given block, for
// forward or reverse transformations.
+ (instancetype)reversibleTransformerWithBlock:(MTLValueTransformerBlock)transformationBlock;
// Returns a transformer which transforms values using the given blocks.
+ (instancetype)reversibleTransformerWithForwardBlock:(MTLValueTransformerBlock)forwardBlock reverseBlock:(MTLValueTransformerBlock)reverseBlock;
@end
@@ -0,0 +1,26 @@
//
// Mantle.h
// Mantle
//
// Created by Justin Spahr-Summers on 2012-09-04.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
//! Project version number for Mantle.
FOUNDATION_EXPORT double MantleVersionNumber;
//! Project version string for Mantle.
FOUNDATION_EXPORT const unsigned char MantleVersionString[];
#import <Mantle/MTLJSONAdapter.h>
#import <Mantle/MTLManagedObjectAdapter.h>
#import <Mantle/MTLModel.h>
#import <Mantle/MTLModel+NSCoding.h>
#import <Mantle/MTLValueTransformer.h>
#import <Mantle/NSArray+MTLManipulationAdditions.h>
#import <Mantle/NSDictionary+MTLManipulationAdditions.h>
#import <Mantle/NSObject+MTLComparisonAdditions.h>
#import <Mantle/NSValueTransformer+MTLInversionAdditions.h>
#import <Mantle/NSValueTransformer+MTLPredefinedTransformerAdditions.h>
@@ -0,0 +1,28 @@
//
// NSArray+MTLManipulationAdditions.h
// Mantle
//
// Created by Josh Abernathy on 9/19/12.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSArray (MTLManipulationAdditions)
// The first object in the array or nil if the array is empty.
// Forwards to `firstObject` which has been first declared in iOS7, but works with iOS4/10.6.
@property (nonatomic, readonly, strong) id mtl_firstObject;
// Returns a new array without all instances of the given object.
- (NSArray *)mtl_arrayByRemovingObject:(id)object;
// Returns a new array without the first object. If the array is empty, it
// returns the empty array.
- (NSArray *)mtl_arrayByRemovingFirstObject;
// Returns a new array without the last object. If the array is empty, it
// returns the empty array.
- (NSArray *)mtl_arrayByRemovingLastObject;
@end
@@ -0,0 +1,25 @@
//
// NSDictionary+MTLManipulationAdditions.h
// Mantle
//
// Created by Justin Spahr-Summers on 2012-09-24.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSDictionary (MTLManipulationAdditions)
// Merges the keys and values from the given dictionary into the receiver. If
// both the receiver and `dictionary` have a given key, the value from
// `dictionary` is used.
//
// Returns a new dictionary containing the entries of the receiver combined with
// those of `dictionary`.
- (NSDictionary *)mtl_dictionaryByAddingEntriesFromDictionary:(NSDictionary *)dictionary;
// Creates a new dictionary with all the entries for the given keys removed from
// the receiver.
- (NSDictionary *)mtl_dictionaryByRemovingEntriesWithKeys:(NSSet *)keys;
@end
@@ -0,0 +1,15 @@
//
// NSObject+MTLComparisonAdditions.h
// Mantle
//
// Created by Josh Vera on 10/26/12.
// Copyright (c) 2012 GitHub. All rights reserved.
//
// Portions copyright (c) 2011 Bitswift. All rights reserved.
// See the LICENSE file for more information.
//
#import <Foundation/Foundation.h>
// Returns whether both objects are identical or equal via -isEqual:
BOOL MTLEqualObjects(id obj1, id obj2);
@@ -0,0 +1,21 @@
//
// NSValueTransformer+MTLInversionAdditions.h
// Mantle
//
// Created by Justin Spahr-Summers on 2013-05-18.
// Copyright (c) 2013 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSValueTransformer (MTLInversionAdditions)
// Flips the direction of the receiver's transformation, such that
// -transformedValue: will become -reverseTransformedValue:, and vice-versa.
//
// The receiver must allow reverse transformation.
//
// Returns an inverted transformer.
- (NSValueTransformer *)mtl_invertedTransformer;
@end
@@ -0,0 +1,84 @@
//
// NSValueTransformer+MTLPredefinedTransformerAdditions.h
// Mantle
//
// Created by Justin Spahr-Summers on 2012-09-27.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
// The name for a value transformer that converts strings into URLs and back.
extern NSString * const MTLURLValueTransformerName;
// Ensure an NSNumber is backed by __NSCFBoolean/CFBooleanRef
//
// NSJSONSerialization, and likely other serialization libraries, ordinarily
// serialize NSNumbers as numbers, and thus booleans would be serialized as
// 0/1. The exception is when the NSNumber is backed by __NSCFBoolean, which,
// though very much an implementation detail, is detected and serialized as a
// proper boolean.
extern NSString * const MTLBooleanValueTransformerName;
@interface NSValueTransformer (MTLPredefinedTransformerAdditions)
// Creates a reversible transformer to convert a JSON dictionary into a MTLModel
// object, and vice-versa.
//
// modelClass - The MTLModel subclass to attempt to parse from the JSON. This
// class must conform to <MTLJSONSerializing>. This argument must
// not be nil.
//
// Returns a reversible transformer which uses MTLJSONAdapter for transforming
// values back and forth.
+ (NSValueTransformer *)mtl_JSONDictionaryTransformerWithModelClass:(Class)modelClass;
// Creates a reversible transformer to convert an array of JSON dictionaries
// into an array of MTLModel objects, and vice-versa.
//
// modelClass - The MTLModel subclass to attempt to parse from each JSON
// dictionary. This class must conform to <MTLJSONSerializing>.
// This argument must not be nil.
//
// Returns a reversible transformer which uses MTLJSONAdapter for transforming
// array elements back and forth.
+ (NSValueTransformer *)mtl_JSONArrayTransformerWithModelClass:(Class)modelClass;
// A reversible value transformer to transform between the keys and objects of a
// dictionary.
//
// dictionary - The dictionary whose keys and values should be
// transformed between. This argument must not be nil.
// defaultValue - The result to fall back to, in case no key matching the
// input value was found during a forward transformation.
// reverseDefaultValue - The result to fall back to, in case no value matching
// the input value was found during a reverse
// transformation.
//
// Can for example be used for transforming between enum values and their string
// representation.
//
// NSValueTransformer *valueTransformer = [NSValueTransformer mtl_valueMappingTransformerWithDictionary:@{
// @"foo": @(EnumDataTypeFoo),
// @"bar": @(EnumDataTypeBar),
// } defaultValue: @(EnumDataTypeUndefined) reverseDefaultValue: @"undefined"];
//
// Returns a transformer that will map from keys to values in dictionary
// for forward transformation, and from values to keys for reverse
// transformations. If no matching key or value can be found, the respective
// default value is returned.
+ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary defaultValue:(id)defaultValue reverseDefaultValue:(id)reverseDefaultValue;
// Returns a value transformer created by calling
// `+mtl_valueMappingTransformerWithDictionary:defaultValue:reverseDefaultValue:`
// with a default value of `nil` and a reverse default value of `nil`.
+ (NSValueTransformer *)mtl_valueMappingTransformerWithDictionary:(NSDictionary *)dictionary;
@end
@interface NSValueTransformer (UnavailableMTLPredefinedTransformerAdditions)
+ (NSValueTransformer *)mtl_externalRepresentationTransformerWithModelClass:(Class)modelClass __attribute__((deprecated("Replaced by +mtl_JSONDictionaryTransformerWithModelClass:")));
+ (NSValueTransformer *)mtl_externalRepresentationArrayTransformerWithModelClass:(Class)modelClass __attribute__((deprecated("Replaced by +mtl_JSONArrayTransformerWithModelClass:")));
@end
@@ -0,0 +1,6 @@
framework module Mantle {
umbrella header "Mantle.h"
export *
module * { export * }
}
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>16F2104</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>Mantle</string>
<key>CFBundleIdentifier</key>
<string>org.mantle.Mantle</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Mantle</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string></string>
<key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key>
<string>8E3004b</string>
<key>DTPlatformVersion</key>
<string>GM</string>
<key>DTSDKBuild</key>
<string>16E185</string>
<key>DTSDKName</key>
<string>macosx10.12</string>
<key>DTXcode</key>
<string>0833</string>
<key>DTXcodeBuild</key>
<string>8E3004b</string>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
</dict>
</plist>
@@ -0,0 +1,264 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
5gOxsGY3VGVvebnR8eP1z3BDrls=
</data>
</dict>
<key>files2</key>
<dict>
<key>Headers/MTLJSONAdapter.h</key>
<dict>
<key>hash</key>
<data>
HIypP9pGI7Wk97RKy3thGsC7LSg=
</data>
<key>hash2</key>
<data>
YhksDrIzFk7TQllZD4eF2qEubtCfFlDrddfBeJEvpoc=
</data>
</dict>
<key>Headers/MTLManagedObjectAdapter.h</key>
<dict>
<key>hash</key>
<data>
tjTyEh7TlZBAXcyaY1I1SmXHbE4=
</data>
<key>hash2</key>
<data>
WxYp7uqr+H3R5UnwMpDSHlxSt7MaAPhQob/BDXlWbVI=
</data>
</dict>
<key>Headers/MTLModel+NSCoding.h</key>
<dict>
<key>hash</key>
<data>
2tdybjH/C8ZU8VcZrX4oliXkuM8=
</data>
<key>hash2</key>
<data>
QuzPKm76puk2/uZN0s9Jqn7g5AFF6xFbnBsN450IXfc=
</data>
</dict>
<key>Headers/MTLModel.h</key>
<dict>
<key>hash</key>
<data>
9c2w4hZo7o/vLu/3TaZ2vXXjwxc=
</data>
<key>hash2</key>
<data>
2JEc6fCePU0odH+dDY8947O0/wS1Ey96Vuun1HCP564=
</data>
</dict>
<key>Headers/MTLValueTransformer.h</key>
<dict>
<key>hash</key>
<data>
m6veNGSL5/Kif8yDXH5zU+2CaK8=
</data>
<key>hash2</key>
<data>
AnO4t0Ur5lBz44l14XRXgB3g6JEiUE1JBfPdvV+H7fA=
</data>
</dict>
<key>Headers/Mantle.h</key>
<dict>
<key>hash</key>
<data>
TYIHtg+qqzl8yG0InqTB7Oyr63A=
</data>
<key>hash2</key>
<data>
YKN3UxwC4VdZ19A/VN8Ndpdf5PVHbZlYMb9K1etJKKg=
</data>
</dict>
<key>Headers/NSArray+MTLManipulationAdditions.h</key>
<dict>
<key>hash</key>
<data>
JkGLHSSIKlmCin6flems7m4llvE=
</data>
<key>hash2</key>
<data>
fQTG4WuuoxdNhgn6wDU29iy18nNqFwlK9F45NRbpD6w=
</data>
</dict>
<key>Headers/NSDictionary+MTLManipulationAdditions.h</key>
<dict>
<key>hash</key>
<data>
bqGP1ngOZeJ16AY5Vi/2ObZCmlw=
</data>
<key>hash2</key>
<data>
0Jf2Da8WIPOoV0BkwF1D69nsUwbgypRDMVoedgtuJ24=
</data>
</dict>
<key>Headers/NSObject+MTLComparisonAdditions.h</key>
<dict>
<key>hash</key>
<data>
2oBONz9+tBIWVjYGxYBIBT0b0n8=
</data>
<key>hash2</key>
<data>
86zL7jX96Spa7xT5HQJ+aYmtXD1oKLUrU4JtlO2cIqU=
</data>
</dict>
<key>Headers/NSValueTransformer+MTLInversionAdditions.h</key>
<dict>
<key>hash</key>
<data>
sxXToKdUH8yMt45aTwPeM3hKmjs=
</data>
<key>hash2</key>
<data>
6xpkrCw6Z2B3EOFKGMBi6o78Hau0v0zsfGJeQMAk354=
</data>
</dict>
<key>Headers/NSValueTransformer+MTLPredefinedTransformerAdditions.h</key>
<dict>
<key>hash</key>
<data>
HhBKFxBsCIBfvD6Rm6d3yjVk540=
</data>
<key>hash2</key>
<data>
QAg6gyiNeq39mqhxZ/OttPualO+F1x2E99sNuyNE1ZE=
</data>
</dict>
<key>Modules/module.modulemap</key>
<dict>
<key>hash</key>
<data>
Bey/HDj6mfWAMaHrd42tLo2G6HA=
</data>
<key>hash2</key>
<data>
kjfaRu6wmQ32w9Db79wuDbkttAjOMC2jrLbtCbIzrSM=
</data>
</dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash</key>
<data>
5gOxsGY3VGVvebnR8eP1z3BDrls=
</data>
<key>hash2</key>
<data>
e6zwAZD1Y0oZhr5vq5p/9RM9J6llqdDsogAev9b3loI=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>
@@ -0,0 +1 @@
Versions/Current/Headers
@@ -0,0 +1 @@
Versions/Current/Modules
@@ -0,0 +1 @@
Versions/Current/ReactiveCocoa
@@ -0,0 +1 @@
Versions/Current/Resources
@@ -0,0 +1,68 @@
//
// EXTKeyPathCoding.h
// extobjc
//
// Created by Justin Spahr-Summers on 19.06.12.
// Copyright (C) 2012 Justin Spahr-Summers.
// Released under the MIT license.
//
#import <Foundation/Foundation.h>
#import "metamacros.h"
/**
* \@keypath allows compile-time verification of key paths. Given a real object
* receiver and key path:
*
* @code
NSString *UTF8StringPath = @keypath(str.lowercaseString.UTF8String);
// => @"lowercaseString.UTF8String"
NSString *versionPath = @keypath(NSObject, version);
// => @"version"
NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString);
// => @"lowercaseString"
* @endcode
*
* ... the macro returns an \c NSString containing all but the first path
* component or argument (e.g., @"lowercaseString.UTF8String", @"version").
*
* In addition to simply creating a key path, this macro ensures that the key
* path is valid at compile-time (causing a syntax error if not), and supports
* refactoring, such that changing the name of the property will also update any
* uses of \@keypath.
*/
#define keypath(...) \
metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__))
#define keypath1(PATH) \
(((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1))
#define keypath2(OBJ, PATH) \
(((void)(NO && ((void)OBJ.PATH, NO)), # PATH))
/**
* \@collectionKeypath allows compile-time verification of key paths across collections NSArray/NSSet etc. Given a real object
* receiver, collection object receiver and related keypaths:
*
* @code
NSString *employessFirstNamePath = @collectionKeypath(department.employees, Employee.new, firstName)
// => @"employees.firstName"
NSString *employessFirstNamePath = @collectionKeypath(Department.new, employees, Employee.new, firstName)
// => @"employees.firstName"
* @endcode
*
*/
#define collectionKeypath(...) \
metamacro_if_eq(3, metamacro_argcount(__VA_ARGS__))(collectionKeypath3(__VA_ARGS__))(collectionKeypath4(__VA_ARGS__))
#define collectionKeypath3(PATH, COLLECTION_OBJECT, COLLECTION_PATH) ([[NSString stringWithFormat:@"%s.%s",keypath(PATH), keypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String])
#define collectionKeypath4(OBJ, PATH, COLLECTION_OBJECT, COLLECTION_PATH) ([[NSString stringWithFormat:@"%s.%s",keypath(OBJ, PATH), keypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String])
@@ -0,0 +1,118 @@
//
// EXTScope.h
// extobjc
//
// Created by Justin Spahr-Summers on 2011-05-04.
// Copyright (C) 2012 Justin Spahr-Summers.
// Released under the MIT license.
//
#import "metamacros.h"
/**
* \@onExit defines some code to be executed when the current scope exits. The
* code must be enclosed in braces and terminated with a semicolon, and will be
* executed regardless of how the scope is exited, including from exceptions,
* \c goto, \c return, \c break, and \c continue.
*
* Provided code will go into a block to be executed later. Keep this in mind as
* it pertains to memory management, restrictions on assignment, etc. Because
* the code is used within a block, \c return is a legal (though perhaps
* confusing) way to exit the cleanup block early.
*
* Multiple \@onExit statements in the same scope are executed in reverse
* lexical order. This helps when pairing resource acquisition with \@onExit
* statements, as it guarantees teardown in the opposite order of acquisition.
*
* @note This statement cannot be used within scopes defined without braces
* (like a one line \c if). In practice, this is not an issue, since \@onExit is
* a useless construct in such a case anyways.
*/
#define onExit \
rac_keywordify \
__strong rac_cleanupBlock_t metamacro_concat(rac_exitBlock_, __LINE__) __attribute__((cleanup(rac_executeCleanupBlock), unused)) = ^
/**
* Creates \c __weak shadow variables for each of the variables provided as
* arguments, which can later be made strong again with #strongify.
*
* This is typically used to weakly reference variables in a block, but then
* ensure that the variables stay alive during the actual execution of the block
* (if they were live upon entry).
*
* See #strongify for an example of usage.
*/
#define weakify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__)
/**
* Like #weakify, but uses \c __unsafe_unretained instead, for targets or
* classes that do not support weak references.
*/
#define unsafeify(...) \
rac_keywordify \
metamacro_foreach_cxt(rac_weakify_,, __unsafe_unretained, __VA_ARGS__)
/**
* Strongly references each of the variables provided as arguments, which must
* have previously been passed to #weakify.
*
* The strong references created will shadow the original variable names, such
* that the original names can be used without issue (and a significantly
* reduced risk of retain cycles) in the current scope.
*
* @code
id foo = [[NSObject alloc] init];
id bar = [[NSObject alloc] init];
@weakify(foo, bar);
// this block will not keep 'foo' or 'bar' alive
BOOL (^matchesFooOrBar)(id) = ^ BOOL (id obj){
// but now, upon entry, 'foo' and 'bar' will stay alive until the block has
// finished executing
@strongify(foo, bar);
return [foo isEqual:obj] || [bar isEqual:obj];
};
* @endcode
*/
#define strongify(...) \
rac_keywordify \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
metamacro_foreach(rac_strongify_,, __VA_ARGS__) \
_Pragma("clang diagnostic pop")
/*** implementation details follow ***/
typedef void (^rac_cleanupBlock_t)();
static inline void rac_executeCleanupBlock (__strong rac_cleanupBlock_t *block) {
(*block)();
}
#define rac_weakify_(INDEX, CONTEXT, VAR) \
CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR);
#define rac_strongify_(INDEX, VAR) \
__strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_);
// Details about the choice of backing keyword:
//
// The use of @try/@catch/@finally can cause the compiler to suppress
// return-type warnings.
// The use of @autoreleasepool {} is not optimized away by the compiler,
// resulting in superfluous creation of autorelease pools.
//
// Since neither option is perfect, and with no other alternatives, the
// compromise is to use @autorelease in DEBUG builds to maintain compiler
// analysis, and to use @try/@catch otherwise to avoid insertion of unnecessary
// autorelease pools.
#if DEBUG
#define rac_keywordify autoreleasepool {}
#else
#define rac_keywordify try {} @catch (...) {}
#endif
@@ -0,0 +1,20 @@
//
// NSArray+RACSequenceAdditions.h
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2012-10-29.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSequence;
@interface NSArray (RACSequenceAdditions)
/// Creates and returns a sequence corresponding to the receiver.
///
/// Mutating the receiver will not affect the sequence after it's been created.
@property (nonatomic, copy, readonly) RACSequence *rac_sequence;
@end
@@ -0,0 +1,22 @@
//
// NSControl+RACCommandSupport.h
// ReactiveCocoa
//
// Created by Josh Abernathy on 3/3/12.
// Copyright (c) 2012 GitHub, Inc. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@class RACCommand;
@interface NSControl (RACCommandSupport)
/// Sets the control's command. When the control is clicked, the command is
/// executed with the sender of the event. The control's enabledness is bound
/// to the command's `canExecute`.
///
/// Note: this will reset the control's target and action.
@property (nonatomic, strong) RACCommand *rac_command;
@end
@@ -0,0 +1,24 @@
//
// NSControl+RACTextSignalSupport.h
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2013-03-08.
// Copyright (c) 2013 GitHub, Inc. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@class RACSignal;
@interface NSControl (RACTextSignalSupport)
/// Observes a text-based control for changes.
///
/// Using this method on a control without editable text is considered undefined
/// behavior.
///
/// Returns a signal which sends the current string value of the receiver, then
/// the new value any time it changes.
- (RACSignal *)rac_textSignal;
@end
@@ -0,0 +1,22 @@
//
// NSData+RACSupport.h
// ReactiveCocoa
//
// Created by Josh Abernathy on 5/11/12.
// Copyright (c) 2012 GitHub, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACScheduler;
@class RACSignal;
@interface NSData (RACSupport)
// Read the data at the URL using -[NSData initWithContentsOfURL:options:error:].
// Sends the data or the error.
//
// scheduler - cannot be nil.
+ (RACSignal *)rac_readContentsOfURL:(NSURL *)URL options:(NSDataReadingOptions)options scheduler:(RACScheduler *)scheduler;
@end
@@ -0,0 +1,31 @@
//
// NSDictionary+RACSequenceAdditions.h
// ReactiveCocoa
//
// Created by Justin Spahr-Summers on 2012-10-29.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSequence;
@interface NSDictionary (RACSequenceAdditions)
/// Creates and returns a sequence of RACTuple key/value pairs. The key will be
/// the first element in the tuple, and the value will be the second.
///
/// Mutating the receiver will not affect the sequence after it's been created.
@property (nonatomic, copy, readonly) RACSequence *rac_sequence;
/// Creates and returns a sequence corresponding to the keys in the receiver.
///
/// Mutating the receiver will not affect the sequence after it's been created.
@property (nonatomic, copy, readonly) RACSequence *rac_keySequence;
/// Creates and returns a sequence corresponding to the values in the receiver.
///
/// Mutating the receiver will not affect the sequence after it's been created.
@property (nonatomic, copy, readonly) RACSequence *rac_valueSequence;
@end
@@ -0,0 +1,20 @@
//
// NSEnumerator+RACSequenceAdditions.h
// ReactiveCocoa
//
// Created by Uri Baghin on 07/01/2013.
// Copyright (c) 2013 GitHub, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSequence;
@interface NSEnumerator (RACSequenceAdditions)
/// Creates and returns a sequence corresponding to the receiver.
///
/// The receiver is exhausted lazily as the sequence is enumerated.
@property (nonatomic, copy, readonly) RACSequence *rac_sequence;
@end
@@ -0,0 +1,19 @@
//
// NSFileHandle+RACSupport.h
// ReactiveCocoa
//
// Created by Josh Abernathy on 5/10/12.
// Copyright (c) 2012 GitHub. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSignal;
@interface NSFileHandle (RACSupport)
// Read any available data in the background and send it. Completes when data
// length is <= 0.
- (RACSignal *)rac_readInBackground;
@end
@@ -0,0 +1,21 @@
//
// NSIndexSet+RACSequenceAdditions.h
// ReactiveCocoa
//
// Created by Sergey Gavrilyuk on 12/17/13.
// Copyright (c) 2013 GitHub, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
@class RACSequence;
@interface NSIndexSet (RACSequenceAdditions)
/// Creates and returns a sequence of indexes (as `NSNumber`s) corresponding to
/// the receiver.
///
/// Mutating the receiver will not affect the sequence after it's been created.
@property (nonatomic, copy, readonly) RACSequence *rac_sequence;
@end

Some files were not shown because too many files have changed in this diff Show More