1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-08 04:20:07 +02:00

Compare commits

..

244 Commits

Author SHA1 Message Date
15d75a7f98 🔧 Bump build 2023-02-17 17:21:13 +01:00
c7c5311ff9 🐛 Ensure checkbox shows correct initial state 2023-02-17 17:19:56 +01:00
c93f047909 🔧 Bump build 2023-02-15 20:32:56 +01:00
f82a2120f7 🔧 Add display name 2023-02-15 20:32:37 +01:00
857cba9f45 On macOS 13 and newer, add "Start at login" (#210) 2023-02-15 20:24:01 +01:00
ec49257bcc 🔧 Bump build 2023-02-13 17:43:52 +01:00
9c6a21008a 🐛 Adjusted for new Homebrew JSON output (#235) 2023-02-13 17:38:09 +01:00
5dffbf57d1 👌 Do not load identity asynchronously 2023-02-11 20:46:05 +01:00
21a1d6576e 🔧 Bump build 2023-02-10 19:36:42 +01:00
b08912ce11 Add support for wildcard constraints (#224) 2023-02-10 19:31:07 +01:00
7285d24ef3 👌 Improve first launch onboarding experience 2023-02-07 22:02:34 +01:00
ac60c66bb9 🐛 Add missing strings for update 2023-02-06 19:33:41 +01:00
9a7575790a 🔧 Bump build 2023-02-06 19:12:34 +01:00
cd5cbccb04 🐛 Fix generated script (#231) 2023-02-06 19:10:10 +01:00
20291bf034 📝 Update README about new updater 2023-02-05 18:53:24 +01:00
2c40f433d3 Add updater to project
If you want to see the source code to the updater, you can find it here:
https://github.com/nicoverbruggen/phpmon-updater

Starting with version 6.0, the code of the updater will be included
in this repository.
2023-02-05 18:37:18 +01:00
5ac4817048 👌 Add built app to .gitignore 2023-02-05 18:17:07 +01:00
1216fe4974 🐛 Avoid duplicate verbose mode output 2023-01-31 18:20:44 +01:00
147407666d 🔥 main branch cleanup
After the last merge, there was one file I accidentally included that
doesn't need to be here: the legacy ServicesManager class! In order to
ensure that 5.7, 6.0 and `main` branches are somewhat in order and
easy to merge, I have now removed this file.
2023-01-31 18:15:00 +01:00
4568f03a65 🚀 Version 5.7.2 2023-01-30 19:54:09 +01:00
2eda8d6382 📝 Update about verbose logging 2023-01-30 19:24:46 +01:00
dd330fecce 🔧 Bump build 2023-01-30 19:12:03 +01:00
aaa7c636db 👌 Remove unneeded print() statement 2023-01-30 19:11:47 +01:00
ff75fb7be3 🐛 Fix version parsing (#227) 2023-01-30 19:11:08 +01:00
4d7b01831b Extra verbose logging
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose`

Once this file exists, you can find the latest log in: `~/.config/phpmon/last_session.log`.
2023-01-30 19:10:52 +01:00
0fceb852bb Add test to validate Valet version number with deprecations 2023-01-30 13:08:23 +01:00
9fb5f33770 🔧 Extra CLI mode (--cli) 2023-01-30 13:07:37 +01:00
2f658ee569 🚀 Version 5.7.1 2023-01-29 15:00:09 +01:00
ad179a325a 🔧 Bump build 2023-01-29 14:59:52 +01:00
4baeaea85f 🐛 Fix services not shutting down (#225) 2023-01-29 14:58:09 +01:00
9e25254ec8 🚀 Version 5.7 2023-01-26 20:49:47 +01:00
e623207844 🔧 New build 2023-01-26 20:32:39 +01:00
f60ecb877c ♻️ Cleanup 2023-01-26 20:30:00 +01:00
66393094b0 Correctly parse .valetrc files 2023-01-24 19:47:28 +01:00
b5d2fef184 🏗 WIP: Add test for feature to be implemented 2023-01-20 16:42:50 +01:00
44bc07c9dc 🏗 WIP: Parsing .valetrc file 2023-01-20 16:36:56 +01:00
93790f3951 👌 Update copyright message 2023-01-19 18:09:42 +01:00
d3b1afe9fd 🐛 Fix bug related to "?" not showing up 2023-01-19 18:03:00 +01:00
2fa50a7dc4 🔧 Bump build for new pre-release build 2023-01-19 17:33:55 +01:00
18b62ecc3f 👌 Adjust SECURITY and support matrix for Valet 4
This commit ensures that PHP Monitor knows about which versions of
PHP are supported by the upcoming Valet 4.0: PHP 7.1 and higher.

Ensures compatibility with https://github.com/laravel/valet/pull/1318
2023-01-19 17:30:21 +01:00
2d59b8c6e8 👌 Improve onboarding 2023-01-18 20:42:24 +01:00
450d7ec001 👌 Fixed onboarding for initial launch 2023-01-18 20:27:02 +01:00
b6b1174ca3 👌 More compact ServicesView 2023-01-18 19:56:54 +01:00
e509f6b59d 📝 TL QC Pass 1 2023-01-18 19:22:45 +01:00
6014320441 📝 Update information about supported PHP versions 2023-01-18 19:18:25 +01:00
a0d80423e9 📝 Update documentation
* Updated README
* Updated SECURITY
* Revert the minimum Valet version to v2.16
2023-01-18 19:10:43 +01:00
35b19efc3e 👌 Use specific theme colors for services status 2023-01-17 18:05:22 +01:00
9a175e7291 🔧 Bump build for new pre-release build 2023-01-16 21:10:58 +01:00
d53e92ce94 👌 Sort PHP versions, amend message 2023-01-16 21:09:25 +01:00
3344fdb1dc 👌 Update support matrix 2023-01-13 20:01:09 +01:00
020a0260f1 Tell users about older unsupported PHP versions 2023-01-13 19:48:02 +01:00
3ce7e8f48b 🔧 Keep track of unsupported (but installed) PHP versions 2023-01-13 19:13:43 +01:00
ae7e13de9b 🔧 Bump build for new pre-release build 2023-01-12 17:20:58 +01:00
c82ea1fac1 🐛 Fix concurrency crashes with @objc methods 2023-01-12 17:20:15 +01:00
d6258f54a9 🐛 Fix crash when Valet.shared is nil 2023-01-11 22:23:39 +01:00
e6d2c873a5 👌 Add modal to inform about helper scripts 2023-01-11 18:03:10 +01:00
300b10c5d8 🐛 Fix crash with check for updates 2023-01-11 17:50:55 +01:00
6eea08cd4f 👌 Finalize PHP Guard functionality 2023-01-10 17:53:55 +01:00
3c946a53e8 👌 Keep track of last used global PHP version 2023-01-09 19:19:18 +01:00
d8738b685f 👌 Get rid of warnings 2023-01-09 18:10:51 +01:00
55fc90bcf5 👌 Better alerts for error state 2023-01-08 13:14:18 +01:00
6a2f4d248c 👌 Improved error handling 2023-01-08 12:48:47 +01:00
a50eb04f3c 👌 Services now report error status 2023-01-08 11:50:01 +01:00
4cbfbeb4e5 👌 Add copy about inactive services 2023-01-07 23:54:01 +01:00
18dd597d38 👌 SwiftUI fixes 2023-01-07 19:01:31 +01:00
e5c80ab52f 👌 Resolve height issues 2023-01-07 18:54:07 +01:00
0b3a83c1e4 👌 Cleanup 2023-01-07 18:10:31 +01:00
6e7c0d827c 👌 Initial loading state for services manager 2023-01-07 17:16:51 +01:00
d05f39efe7 👌 Check if Valet version is supported 2023-01-07 13:24:52 +01:00
27894e4884 👌 Handle TODOs 2023-01-07 13:03:27 +01:00
f153fee05c Fix broken tests after test config using 8.2 2023-01-07 12:57:45 +01:00
71e1ed1b93 👌 Async switcher (Swift concurrency) 2023-01-07 12:53:27 +01:00
61ecefb6e7 👌 Use filesystem abstraction 2023-01-07 12:53:07 +01:00
422a7738bd 👌 Add fake services system 2023-01-06 21:50:34 +01:00
0beda388eb 👌 Cleanup 2023-01-06 21:33:51 +01:00
684a53fc4a 🏗 WIP: Functional service toggling 2023-01-06 20:30:25 +01:00
456948ffd9 🏗 WIP: Broken services toggling
You can break it by opening the menu twice.
2023-01-06 19:14:17 +01:00
a090cbc20b 👌 Revert loading 2023-01-06 18:34:39 +01:00
7b07520440 🏗 WIP: Service toggling 2023-01-06 18:33:55 +01:00
6d52992c9d 👌 Add comments about concurrency 2023-01-06 13:07:02 +01:00
291d20b2b3 🏗 WIP: Functional services list 2023-01-06 12:59:15 +01:00
3d505aebde 🚛 VersionNumber, PhpVersionNumberCollection 2023-01-06 12:41:37 +01:00
d854dee2a1 Fix tests related to version number parsing 2023-01-05 17:58:55 +01:00
67e8589834 ♻️ Alter how PHP version support is handled 2023-01-05 17:45:23 +01:00
a36c9b1563 📝 Update documentation 2023-01-05 12:17:31 +01:00
df1b1c5856 🏗️ Investigate event propagation 2023-01-03 20:17:29 +01:00
5af7214320 🏗️ Layout changes 2023-01-03 20:15:44 +01:00
e20d3ffd22 🏗️ WIP: Various fixes and improvements
- Fixed `brewPhpAlias` (must be configurable later)
- Added TODOs for where the filesystem abstraction is required
- Set `Homebrew.fake` early on when applying testable configuration
- Evaluate `FakeValetSite` compatibility again
- Never display sponsor alert when running tests
- Upgrade TestableConfiguration.working to use PHP 8.2
2023-01-03 19:29:44 +01:00
be70559d4c 🏗 WIP: Adjust dimensions 2023-01-03 16:58:48 +01:00
61988a141b 🏗 WIP: Center buttons and checkmarks 2023-01-03 16:14:45 +01:00
3bcf52bf0a 🏗 WIP: Use objectWillChange.send() 2023-01-03 16:11:55 +01:00
296bc486c4 🏗 WIP: Fake services manager 2022-12-24 15:17:47 +01:00
59f60b5013 🏗 WIP: Services manager main status
Please note that these are computed properties.
They should be computed when a service status
is modified, and should be `Published`.
2022-12-24 15:04:25 +01:00
de2c1aca5d 🏗 More SwiftUI experiments 2022-12-24 14:51:23 +01:00
44c1ea7de4 🏗 SwiftUI experiments 2022-12-23 19:20:04 +01:00
923f0237e8 🔥 WIP: Removed broken view 2022-12-20 19:42:27 +01:00
ff7c68ddfb 🏗 WIP: Services rewrite 2022-12-19 18:09:10 +01:00
bf2c0c259f 👌 Mastodon > Twitter 2022-12-19 17:35:32 +01:00
0fdd1264ed 👌 Await the outcome of cash conflict check 2022-12-19 17:30:56 +01:00
c85a8e3818 Add test for tapping on "add domain" 2022-12-18 14:50:03 +01:00
15fe5e4716 Add tests for domain list interaction 2022-12-16 20:50:53 +01:00
de6dea066e Use ValetInteractor to add links and proxies 2022-12-16 20:20:31 +01:00
ee230f3086 👌 Unify scanners 2022-12-15 22:28:15 +01:00
bc96b50630 Add fake isolation interaction 2022-12-14 20:28:49 +01:00
d6554ceea9 Add site isolation to ValetInteractor 2022-12-14 20:18:06 +01:00
4b04f70638 👌 Fix phrasing ("site is unsecured")
A site can be either insecure or unsecured. Insecure has a rather
negative connotation and is also a human trait. Unsecured is a better 
term in this particular case.
2022-12-13 23:49:55 +01:00
d49e74fab1 🚛 Move FakeValetInteractor to separate file 2022-12-13 23:47:59 +01:00
e34dadcb9b 👌 Reinstate UI busy for secure, add fake delay 2022-12-13 23:45:19 +01:00
a696a9a386 👌 Style fix 2022-12-13 23:32:50 +01:00
837392d606 Add unlinking sites for fake links 2022-12-13 23:32:19 +01:00
912d9e7423 Add FakeValetInteractor 2022-12-13 23:21:15 +01:00
cb98d40bef 👌 Fix crash with new secure/unsecure mechanism 2022-12-13 23:02:30 +01:00
1f165058b2 ♻️ Rework interaction with Valet commands 2022-12-13 22:56:36 +01:00
04c78eba35 👌 Generate a new JSON file for current dev env 2022-12-13 20:24:48 +01:00
4bfde7b062 👌 Add warning about https proxy subjects 2022-12-13 20:21:34 +01:00
00cc6711a1 🚀 Version 5.6.6 2022-12-10 13:04:16 +01:00
209244e162 🔧 Bump build 2022-12-09 18:27:19 +01:00
e48d8ed98b 👌 Add warning about https proxy subjects 2022-12-09 18:26:56 +01:00
834f76e5a1 📝 Updated README about PHP compatibility 2022-12-09 18:08:05 +01:00
5f39cd757a 📝 Update README 2022-12-09 18:05:31 +01:00
7e48893247 📝 Update README 2022-12-09 18:04:05 +01:00
073b7cf943 🏗 WIP: Interactions 2022-12-06 20:53:42 +01:00
31a0bb986f ♻️ Some odd refactoring 2022-12-06 19:43:45 +01:00
81183acea8 ♻️ Cleanup 2022-12-06 19:28:20 +01:00
e29ca9e1ad 🚀 Version 5.6.5 2022-11-27 14:37:15 +01:00
40e05d9445 🔧 Bump build 2022-11-27 14:04:15 +01:00
3ebf51b319 👌 Fix threading issue with Composer update (#212) 2022-11-27 14:03:46 +01:00
3f8739dc30 👌 Fix timing issue 2022-11-22 00:14:46 +01:00
c3e55df9fb WIP: Add support for nginx-full, formulae tweaks 2022-11-22 00:01:31 +01:00
4b8ad911f1 Add support for nginx-full (#211) 2022-11-21 18:10:33 +01:00
c6aa06842c 🔥 Remove reference to LatestStablePhpVersion 2022-11-18 19:18:36 +01:00
efd902b4f3 🚀 Version 5.6.4 2022-11-18 18:39:00 +01:00
918e272da7 🔧 Bump build 2022-11-18 18:37:31 +01:00
272a9182d3 🔥 Remove reference to LatestStablePhpVersion 2022-11-18 18:37:20 +01:00
63f85aff91 🐛 Fix issue when using services quick toggle 2022-11-18 18:32:38 +01:00
a801174f0a Fully cover RealFileSystem 2022-11-09 20:05:15 +01:00
fa5c843619 Add tests for RealFileSystem class 2022-11-08 20:14:10 +01:00
581c2d1974 📝 Update supported PHP versions in README 2022-11-07 20:45:58 +01:00
bc739e1982 👌 Add feature test for InternalSwitcher 2022-11-07 20:42:52 +01:00
a21d928a6c 📝 Update SECURITY.md 2022-11-02 21:22:49 +01:00
4deef64537 📝 Update SECURITY.md 2022-11-02 21:08:57 +01:00
bedabaa3bb 👌 PHP 8.2 release, PHP 8.3-dev support 2022-11-02 21:06:36 +01:00
76d96b3507 Fix issue with UI tests and .localizable 2022-11-02 21:05:12 +01:00
4de7179d1c 👌 Include brew (un)link commands for tests 2022-11-02 20:08:15 +01:00
f2d5b94831 Fix tests 2022-11-02 19:44:36 +01:00
ff5fdd82b1 👌 Add default system "www.conf" file 2022-11-01 17:11:55 +01:00
4f6bae87d4 Add Composer to testable configuration 2022-11-01 17:08:01 +01:00
ce44166b48 Added more tests, added to fake & real FS 2022-11-01 17:02:26 +01:00
5caca85d7a 👌 Fake FS: ~ and intermediate directories 2022-11-01 16:47:45 +01:00
fa2d2105c5 👌 Removed remaining FileManager.default usage 2022-11-01 14:11:34 +01:00
8417d637fe 👌 FileSystem changes, rework and testing 2022-11-01 13:47:16 +01:00
e8c85f93f9 👌 Change where scanners are initialized 2022-11-01 12:10:47 +01:00
24659d4385 👌 Bump recommended Valet version 2022-10-25 23:30:49 +02:00
786b59aa92 🐛 Handle empty output for brew info 2022-10-22 17:39:43 +02:00
9da3772212 🚀 Version 5.6.3 2022-10-22 17:34:53 +02:00
e62b03d070 👌 Style fix 2022-10-22 16:43:05 +02:00
9a11d2efed 🔧 Bump build 2022-10-22 16:42:26 +02:00
b134e62328 🐛 Handle empty output for brew info 2022-10-22 16:41:54 +02:00
771f8dc757 🏗 WIP for fake site enhancements 2022-10-21 19:45:42 +02:00
e18db4eadd 👌 Mark determineVersion as throws 2022-10-21 19:31:05 +02:00
1ece5c34bf Correctly parse pre-release PHP versions 2022-10-21 19:27:31 +02:00
04ed29bc9f 👌 Fix warnings 2022-10-21 18:19:31 +02:00
ac2184ba97 👌 Add brew tap homebrew/services instruction
This now recommends the appropriate solution for #208.
2022-10-20 20:44:54 +02:00
5c69133c42 👌 Add brew tap homebrew/services instruction
This now recommends the appropriate solution for #208.
2022-10-20 20:44:44 +02:00
8310cf2729 👌 Touching up correctness of process handling 2022-10-19 14:57:28 +02:00
696f9bf351 👌 Various async improvements 2022-10-19 14:27:39 +02:00
17b1329d71 👌 Cleanup 2022-10-19 13:52:11 +02:00
4bfa98fc20 ♻️ Refactor DispatchQueue to new Task API 2022-10-19 13:44:53 +02:00
658cec27c1 👌 Cleanup 2022-10-18 23:44:18 +02:00
1c0d9f6826 👌 Swift 6 compatibility 2022-10-18 23:41:46 +02:00
507d7d5b23 Fix PHP version detection requirement 2022-10-18 16:43:17 +02:00
4b8b46a822 Fix test 2022-10-18 14:12:57 +02:00
ea5dd3bc46 👌 Snake case for tests 2022-10-18 14:11:55 +02:00
83657fee6f Tests are final 2022-10-16 15:37:17 +02:00
4c752b6a15 Localization support for in test files 2022-10-16 15:13:13 +02:00
5e3e0c087b 👌 Read configuration from JSON file
This allows us to alter the configuration prior to launching the app,
which allows for additional flexibility during testing.
2022-10-16 14:35:19 +02:00
273070ef27 ️ Sped up and improved UI test 2022-10-15 16:37:33 +02:00
cb3208c008 Real UI test 2022-10-15 15:36:03 +02:00
d401fe997d Make UI test actually functional 2022-10-15 15:14:49 +02:00
eaf6ef658f 🚛 Moved tests around, added Feature, UI tests 2022-10-14 18:03:14 +02:00
d91e16d674 Add test plan, fix unit tests 2022-10-14 17:10:58 +02:00
008603b8c3 👌 Fix project structure 2022-10-14 16:55:37 +02:00
728274aaca 👌 Fix warnings 2022-10-14 16:54:11 +02:00
daaece3cfa Added fake commands 2022-10-12 23:25:06 +02:00
eaa74b7141 👌 Annotate configurations 2022-10-12 22:50:06 +02:00
8a6656d3e2 👌 Improve logging 2022-10-12 22:40:48 +02:00
ad46f51d73 👌 Use fake filesystem 2022-10-12 22:38:03 +02:00
12a4efc775 👌 Improvements to BetterAlert, apply() configs
- MainActor fixes for BetterAlert
- Added `apply` for TestableConfiguration
2022-10-12 22:19:36 +02:00
ec4c4df5fd Add preference to disable TLD alert (#206) 2022-10-12 18:52:44 +02:00
f4448e0640 🔧 Bump version number 2022-10-10 21:49:51 +02:00
7fd30d7c54 Add preference to disable TLD alert (#206) 2022-10-10 21:49:43 +02:00
2c57dea97f 🔀 Merge branch 'main' into dev/5.6 2022-10-09 22:00:36 +02:00
a77fa5557a 📝 Update README for Login Items on Ventura 2022-10-09 21:56:13 +02:00
b0f27dcfa5 ♻️ Filesystem to FileSystem / ActiveFileSystem 2022-10-08 01:04:51 +02:00
f5d2ec2b7e 👌 Add delay global function 2022-10-08 00:27:29 +02:00
6db5cdec25 🐛 Introduce SLOW SHELL and fix a few issues 2022-10-07 22:55:48 +02:00
03cf4ef3e4 👌 Added various TODO items 2022-10-06 23:35:14 +02:00
6feb8118d9 🚛 Move files around 2022-10-06 23:34:31 +02:00
45a82b2c9e 🏗 Checked and fixed various Task { } blocks 2022-10-06 23:29:13 +02:00
ed3622cc4e Fix tests 2022-10-06 23:00:21 +02:00
e2ab7f08ed 🏗 Remove LegacyShell entirely 2022-10-06 22:55:05 +02:00
ad41e3891e 🏗 Mark old Shell as deprecated from now on 2022-10-05 22:39:36 +02:00
108ae05c1d 🏗 Use new shell when parsing apps 2022-10-05 22:38:54 +02:00
fb1ca60240 Fix tests, new shell in various places 2022-10-05 01:07:09 +02:00
3267dc8add ♻️ Use new Shell.pipe to replace legacy shell 2022-10-05 00:38:17 +02:00
1fd7db15a7 🏗 Testable terminal output 2022-10-04 23:42:43 +02:00
0b33116eb0 🏗 Fake shell in use 2022-10-04 19:39:34 +02:00
2c25bcbdb5 🏗 Ensure Shellable has PATH 2022-10-04 19:00:33 +02:00
8a6139d5e7 🏗 Remove synchronous terminal commands 2022-10-04 18:40:41 +02:00
953ccb3792 🏗 WIP 2022-10-04 17:57:05 +02:00
c26c491340 🏗 WIP 2022-10-03 22:27:50 +02:00
c9a5cd3a9f 🏗 WIP 2022-10-03 19:27:14 +02:00
45704fc736 🚀 Version 5.6.2 2022-10-02 13:28:58 +02:00
f28354e634 🐛 Use valet secure sitename (#197) 2022-10-02 13:28:01 +02:00
86eb295489 👌 Add runComposerUpdateShellCommand method 2022-09-30 23:45:32 +02:00
572330eaa1 👌 Remove reference to singleton 2022-09-30 23:44:16 +02:00
5ebafdb4e3 👌 Shell tweaks, fix ComposerWindow async issue 2022-09-29 19:08:40 +02:00
ffffcad84b 🐛 Fix ComposerWindow deinit not firing 2022-09-29 19:00:59 +02:00
8055a32bde 🐛 Fix ComposerWindow deinit not firing 2022-09-29 18:50:40 +02:00
4c11fae541 🏗 WIP: Run shell commands in parallel 2022-09-28 22:18:16 +02:00
99da328921 🏗 WIP: Even better Shell functionality 2022-09-28 21:43:18 +02:00
bbac2632a2 🏗 WIP: Much improved Shell protocol 2022-09-28 21:28:51 +02:00
513a86ec39 🐛 Fix an issue with missing separator item 2022-09-28 18:24:34 +02:00
5b3054326e 🔧 Bump version number 2022-09-28 18:24:26 +02:00
e7f3c7e59c 🐛 Fix an issue with missing separator item 2022-09-28 18:24:01 +02:00
a59efb7fce 🏗 WIP: Shell rework 2022-09-27 22:27:33 +02:00
3483569410 🏗 WIP: Shell rework 2022-09-27 20:26:11 +02:00
5399bddfeb 🏗 WIP: Shell rework 2022-09-26 20:37:24 +02:00
a682d0cfb0 🐛 Merge fixes from 'dev/5.6' into dev/6.0 2022-09-23 16:48:47 +02:00
e871a00490 👌 Add additional commentary to new shell classes 2022-09-21 21:42:22 +02:00
1d396202db 👌 Add PATH to SystemShell 2022-09-21 21:31:00 +02:00
39769d815f 🚛 Move around files 2022-09-21 21:06:11 +02:00
90a69338f7 🏗 Add additional test 2022-09-20 20:53:15 +02:00
3f25759d4f 🏗 Fix SwiftLint, WIP shell rework 2022-09-20 20:49:29 +02:00
4494a0555f 🚧 WIP: Shell rework 2022-09-20 00:33:58 +02:00
0d86f3ded6 📝 Update README, contribution guidelines 2022-09-18 14:09:36 +02:00
fc27131cca 👌 Add support for <= and < version constraints
I am entirely unsure why one would need these, but I figured I'd get
these in the app before I start the work on PHP Monitor 6.0.

This ensures all common version constraints can now be parsed correctly.
2022-09-18 00:06:37 +02:00
bb124bd0ee 👌 Cleanup and removal of unneeded dump 2022-09-17 23:16:49 +02:00
c35e7781f4 🔥 Remove unneeded plist file 2022-09-17 23:14:39 +02:00
61528cea46 🏗 WIP 2022-09-14 19:05:14 +02:00
883f5a1a5d 🔀 Merge branch 'dev/5.6' into dev/6.0 2022-09-10 21:46:17 +02:00
95729c5315 ♻️ Single target, multiple configurations 2022-09-10 21:21:58 +02:00
f02e45486e ♻️ Refactor ActivePhpInstallation 2022-09-09 22:18:52 +02:00
6ddddc744a 🔧 Add target for PHP Monitor SE 2022-09-09 21:46:20 +02:00
244 changed files with 9116 additions and 2995 deletions

View File

@ -29,5 +29,10 @@ If applicable, add screenshots to help explain your problem.
- OS: [e.g. macOS Monterey]
- PHP Monitor version [e.g. v5.0.1]
**Additional log**
You can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it here!
**Additional context**
Add any other context about the problem here.

1
.gitignore vendored
View File

@ -2,4 +2,5 @@ phpmon.xcodeproj/project.xcworkspace
phpmon.xcodeproj/xcuserdata
PHP Monitor.xcodeproj/project.xcworkspace
PHP Monitor.xcodeproj/xcuserdata
phpmon-updater/PHP Monitor Self-Updater.app
.DS_Store

View File

@ -14,6 +14,16 @@ It also automatically runs when you try to build the project. You'll get a warni
swiftlint --fix
```
## ⚙️ Preferences
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
These values are cached by the OS. You can clear this cache by running:
```
defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
```
## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/>

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2019-2022 Nico Verbruggen
Copyright (c) 2019-2023 Nico Verbruggen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug.Dev"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
codeCoverageEnabled = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "Unit Tests.xctest"
BlueprintName = "Unit Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
BuildableName = "UI Tests.xctest"
BlueprintName = "UI Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
BuildableName = "Feature Tests.xctest"
BlueprintName = "Feature Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug.Dev"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--cli"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_working.json"
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
argument = "--configuration:~/.phpmon_fconf_broken.json"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "EXTREME_DOCTOR_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release.Dev"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C41C1B3222B0097F00E7CF16"
BuildableName = "PHP Monitor.app"
BlueprintName = "PHP Monitor"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug.Dev">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release.Dev"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
@ -27,14 +27,42 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:PHP Monitor.xcodeproj/PHP Monitor.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "phpmon-tests.xctest"
BlueprintName = "phpmon-tests"
BuildableName = "Unit Tests.xctest"
BlueprintName = "Unit Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7AC28F9B4940021E251"
BuildableName = "Feature Tests.xctest"
BlueprintName = "Feature Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C471E7BB28F9B90F0021E251"
BuildableName = "UI Tests.xctest"
BlueprintName = "UI Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
@ -73,6 +101,11 @@
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "SLOW_SHELL_MODE"
value = ""
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "PAINT_PHPMON_SWIFTUI_VIEWS"
value = ""

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C4F7807825D7F84B000DBC97"
BuildableName = "Unit Tests.xctest"
BlueprintName = "Unit Tests"
ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -27,24 +27,40 @@ PHP Monitor is a universal application that runs natively on Apple Silicon **and
* macOS 11 Big Sur or later
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* Homebrew `php` formula is installed
* Laravel Valet 3 recommended (but compatible with Valet 2)
* Laravel Valet (works with Valet v2, v3 and v4)
_You may need to update your Valet installation to keep everything working if a major version update of PHP has been released. You can do this by running `composer global update && valet install`. Some features are not supported when running Valet 2._
For more information, please see [SECURITY.md](./SECURITY.md) to find out which version of the app is currently supported.
## 🚀 How to install
Again, make sure you have **Laravel Valet** installed first. Once that's done, you can install via Homebrew (recommended), or may download the latest release on GitHub.
Again, make sure you have **[Laravel Valet](https://laravel.com/docs/master/valet)** installed first:
To install via Homebrew, run:
```sh
composer global require laravel/valet
valet install
valet trust
```
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
#### Manual installation (first time only)
To upgrade your existing installation, run:
Once that's done, you can [download the latest release](https://github.com/nicoverbruggen/phpmon/releases/latest), unzip it and place it in `/Applications`.
brew upgrade phpmon
#### Installation via Homebrew
(You may need to run `brew update` or `brew update-reset` first in order to update the cask file if you ran a Homebrew operation recently.)
If you prefer to install the app via Homebrew, you can also do this:
```sh
brew tap nicoverbruggen/homebrew-cask
brew install --cask phpmon
```
## ⬆️ How to update
The recommended method of updating your app to the latest version is to use **the built-in updater**.
If that doesn't work or you prefer Homebrew, you can also upgrade via those methods.
## ⚡️ Launchers (Alfred, Raycast)
@ -79,20 +95,43 @@ If you're still having issues, here's a few common questions & answers, as well
<details>
<summary><strong>Which versions of PHP are supported?</strong></summary>
<ul>
<li>PHP 5.6 (only if you are running Valet 2)</li>
<li>PHP 7.0</li>
<li>PHP 7.1</li>
<li>PHP 7.2</li>
<li>PHP 7.3</li>
<li>PHP 7.4</li>
<li>PHP 8.0</li>
<li>PHP 8.1</li>
<li>PHP 8.2 (experimental)</li>
</ul>
All stable and supported PHP versions are also supported by PHP Monitor. However, depending on which version of Valet you have installed, which versions of PHP that are made available for switching purposes may differ.
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Common/Core/Constants.swift#L16) file to see which versions are supported.
> **Note**
> If you have versions of PHP installed that can be detected by PHP Monitor but is *not* supported by the currently active version of Valet, you will be alerted by an item in the menu with an exclamation mark emoji. (⚠️)
Backports are available via [this tap](https://github.com/shivammathur/homebrew-php). For more information about those backports, please see the next FAQ entry.
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
</details>
<details>
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
You can install other supported versions of PHP out of the box, so `php@8.0` and `php@8.1` at the time of writing.
If you wish to install older (officially unsupported) versions of PHP for local use, you can do so by using [Shivam Mathur's tap](https://github.com/shivammathur/homebrew-php):
```sh
brew tap shivammathur/php
```
You may find that this tap is already in use: if you've used Valet before, it automatically uses this tap for legacy versions of PHP.
```sh
brew install shivammathur/php/php@7.4
brew install shivammathur/php/php@7.3
brew install shivammathur/php/php@7.2
brew install shivammathur/php/php@7.1
brew install shivammathur/php/php@7.0
```
**Always make sure to restart PHP Monitor after installing or upgrading PHP versions!**
> *Note*: Using this tap may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases.
</details>
<details>
@ -516,6 +555,10 @@ If you would like to report a crash, please include the associated **log files**
To find the logs, take a look in `~/Library/Logs/DiagnosticReports` (in Finder) and see if there's any (log) files that start with "PHP Monitor".
Additionally, you can help me figure out even more information by sending me your verbose log for your latest session of PHP Monitor. Logging is disabled by default.
You can start extra verbose logging by running: `touch ~/.config/phpmon/verbose` and restarting PHP Monitor. You can find the latest log in: `~/.config/phpmon/last_session.log`. Please attach it to the relevant bug report.
</details>
## 📝 Having another issue?

View File

@ -6,9 +6,9 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
| 5.7 | ✅ Universal binary | ✅ Yes | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x*) | 3.0 or higher recommended<br/> 2.16.2 minimum |
_(*) macOS Ventura (13.0) is not officially supported until it officially releases._
(*) Preliminary listing. Valet 4 hasn't been released yet and the versions of PHP Valet can work with might still change.
## Legacy versions
@ -16,6 +16,7 @@ These versions of PHP Monitor are no longer supported, but if youre using an
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0)* | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
| 4.0 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |
| 3.5 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 10.14+ | PHP 5.6—PHP 8.2 | 2.13 |

Binary file not shown.

View File

@ -1,22 +0,0 @@
<?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>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -1,332 +0,0 @@
[
{
"name":"php",
"full_name":"php",
"tap":"homebrew/core",
"oldname":null,
"aliases":[
"php@8.0"
],
"versioned_formulae":[
"php@7.4",
"php@7.3",
"php@7.2"
],
"desc":"General-purpose scripting language",
"license":"PHP-3.01",
"homepage":"https://www.php.net/",
"versions":{
"stable":"8.0.2",
"head":"HEAD",
"bottle":true
},
"urls":{
"stable":{
"url":"https://www.php.net/distributions/php-8.0.2.tar.xz",
"tag":null,
"revision":null
}
},
"revision":0,
"version_scheme":0,
"bottle":{
"stable":{
"rebuild":0,
"cellar":"/opt/homebrew/Cellar",
"prefix":"/opt/homebrew",
"root_url":"https://homebrew.bintray.com/bottles",
"files":{
"arm64_big_sur":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.arm64_big_sur.bottle.tar.gz",
"sha256":"cbefa1db73d08b9af4593a44512b8d727e43033ee8517736bae5f16315501b12"
},
"big_sur":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.big_sur.bottle.tar.gz",
"sha256":"6857142e12254b15da4e74c2986dd24faca57dac8d467b04621db349e277dd63"
},
"catalina":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.catalina.bottle.tar.gz",
"sha256":"b651611134c18f93fdf121a4277b51b197a896a19ccb8020289b4e19e0638349"
},
"mojave":{
"url":"https://homebrew.bintray.com/bottles/php-8.0.2.mojave.bottle.tar.gz",
"sha256":"9583a51fcc6f804aadbb14e18f770d4fb4973deaed6ddc4770342e62974ffbca"
}
}
}
},
"keg_only":false,
"bottle_disabled":false,
"options":[
],
"build_dependencies":[
"httpd",
"pkg-config"
],
"dependencies":[
"apr",
"apr-util",
"argon2",
"aspell",
"autoconf",
"curl",
"freetds",
"gd",
"gettext",
"glib",
"gmp",
"icu4c",
"krb5",
"libffi",
"libpq",
"libsodium",
"libzip",
"oniguruma",
"openldap",
"openssl@1.1",
"pcre2",
"sqlite",
"tidy-html5",
"unixodbc"
],
"recommended_dependencies":[
],
"optional_dependencies":[
],
"uses_from_macos":[
{
"xz":"build"
},
"bzip2",
"libedit",
"libxml2",
"libxslt",
"zlib"
],
"requirements":[
],
"conflicts_with":[
],
"caveats":"To enable PHP in Apache add the following to httpd.conf and restart Apache:\n LoadModule php_module $(brew --prefix)/opt/php/lib/httpd/modules/libphp.so\n\n <FilesMatch \\.php$>\n SetHandler application/x-httpd-php\n </FilesMatch>\n\nFinally, check DirectoryIndex includes index.php\n DirectoryIndex index.php index.html\n\nThe php.ini and php-fpm.ini file can be found in:\n $(brew --prefix)/etc/php/8.0/\n",
"installed":[
{
"version":"8.0.2",
"used_options":[
],
"built_as_bottle":true,
"poured_from_bottle":true,
"runtime_dependencies":[
{
"full_name":"apr",
"version":"1.7.0"
},
{
"full_name":"openssl@1.1",
"version":"1.1.1i"
},
{
"full_name":"apr-util",
"version":"1.6.1"
},
{
"full_name":"argon2",
"version":"20190702"
},
{
"full_name":"aspell",
"version":"0.60.8"
},
{
"full_name":"autoconf",
"version":"2.69"
},
{
"full_name":"brotli",
"version":"1.0.9"
},
{
"full_name":"gettext",
"version":"0.21"
},
{
"full_name":"libunistring",
"version":"0.9.10"
},
{
"full_name":"libidn2",
"version":"2.3.0"
},
{
"full_name":"libmetalink",
"version":"0.1.3"
},
{
"full_name":"libssh2",
"version":"1.9.0"
},
{
"full_name":"c-ares",
"version":"1.17.1"
},
{
"full_name":"jemalloc",
"version":"5.2.1"
},
{
"full_name":"libev",
"version":"4.33"
},
{
"full_name":"nghttp2",
"version":"1.43.0"
},
{
"full_name":"openldap",
"version":"2.4.57"
},
{
"full_name":"rtmpdump",
"version":"2.4+20151223"
},
{
"full_name":"zstd",
"version":"1.4.8"
},
{
"full_name":"curl",
"version":"7.75.0"
},
{
"full_name":"libtool",
"version":"2.4.6"
},
{
"full_name":"unixodbc",
"version":"2.3.9"
},
{
"full_name":"freetds",
"version":"1.2.18"
},
{
"full_name":"libpng",
"version":"1.6.37"
},
{
"full_name":"freetype",
"version":"2.10.4"
},
{
"full_name":"fontconfig",
"version":"2.13.1"
},
{
"full_name":"jpeg",
"version":"9d"
},
{
"full_name":"libtiff",
"version":"4.2.0"
},
{
"full_name":"webp",
"version":"1.2.0"
},
{
"full_name":"gd",
"version":"2.3.1"
},
{
"full_name":"libffi",
"version":"3.3"
},
{
"full_name":"pcre",
"version":"8.44"
},
{
"full_name":"gdbm",
"version":"1.18.1"
},
{
"full_name":"readline",
"version":"8.1"
},
{
"full_name":"sqlite",
"version":"3.34.0"
},
{
"full_name":"tcl-tk",
"version":"8.6.11"
},
{
"full_name":"xz",
"version":"5.2.5"
},
{
"full_name":"python@3.9",
"version":"3.9.1"
},
{
"full_name":"glib",
"version":"2.66.6"
},
{
"full_name":"gmp",
"version":"6.2.1"
},
{
"full_name":"icu4c",
"version":"67.1"
},
{
"full_name":"krb5",
"version":"1.19"
},
{
"full_name":"libpq",
"version":"13.1"
},
{
"full_name":"libsodium",
"version":"1.0.18"
},
{
"full_name":"libzip",
"version":"1.7.3"
},
{
"full_name":"oniguruma",
"version":"6.9.6"
},
{
"full_name":"pcre2",
"version":"10.36"
},
{
"full_name":"tidy-html5",
"version":"5.6.0"
}
],
"installed_as_dependency":false,
"installed_on_request":true
}
],
"linked_keg":"8.0.2",
"pinned":false,
"outdated":false,
"deprecated":false,
"deprecation_date":null,
"deprecation_reason":null,
"disabled":false,
"disable_date":null,
"disable_reason":null
}
]

View File

@ -1,47 +0,0 @@
//
// AppUpdaterCheckTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class AppUpdaterCheckTest: XCTestCase {
func testCanRetrieveVersionFromCask() {
let caskVersion = AppUpdateChecker.retrieveVersionFromCask()
let version = VersionExtractor.from(caskVersion)
XCTAssertNotNil(version)
}
func testTaggedReleaseOmitsZeroPatch() {
let version = AppVersion.from("3.5.0_333")!
XCTAssertEqual(version.tagged, "3.5")
XCTAssertEqual(version.version, "3.5.0")
}
func testTaggedReleaseDoesntOmitNonZeroPatch() {
let version = AppVersion.from("3.5.1_333")!
XCTAssertEqual(version.tagged, "3.5.1")
XCTAssertEqual(version.version, "3.5.1")
}
func testTagTruncationDoesntAffectMajorVersions() {
var version = AppVersion.from("5.0_333")!
XCTAssertEqual(version.tagged, "5.0")
XCTAssertEqual(version.version, "5.0")
version = AppVersion.from("5.0.0_333")!
XCTAssertEqual(version.tagged, "5.0")
XCTAssertEqual(version.version, "5.0.0")
}
}

View File

@ -1,62 +0,0 @@
//
// AppVersionTest.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 10/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class AppVersionTest: XCTestCase {
func testCanRetrieveInternalAppVersion() {
XCTAssertNotNil(AppVersion.fromCurrentVersion())
}
func testCanParseNormalVersionString() {
let version = AppVersion.from("1.0.0")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual(nil, version?.build)
XCTAssertEqual(nil, version?.suffix)
}
func testCanParseCaskVersionString() {
let version = AppVersion.from("1.0.0_600")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("600", version?.build)
XCTAssertEqual(nil, version?.suffix)
}
func testCanParseDevVersionStringWithoutBuildNumber() {
let version = AppVersion.from("1.0.0-dev")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual(nil, version?.build)
XCTAssertEqual("dev", version?.suffix)
}
func testCanParseDevVersionStringWithBuildNumber() {
let version = AppVersion.from("1.0.0-dev,870")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("870", version?.build)
XCTAssertEqual("dev", version?.suffix)
}
func testCanParseUnderscoresAsBuildSeparatorToo() {
let version = AppVersion.from("1.0.0-dev_870")
XCTAssertNotNil(version)
XCTAssertEqual("1.0.0", version?.version)
XCTAssertEqual("870", version?.build)
XCTAssertEqual("dev", version?.suffix)
}
}

View File

@ -1,29 +0,0 @@
//
// PhpVersionDetectionTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class PhpVersionDetectionTest: XCTestCase {
func testCanDetectValidPhpVersions() throws {
let outcome = PhpEnv.shared.extractPhpVersions(from: [
"", // empty lines should be omitted
"php@8.0",
"php@8.0", // should only be detected once
"meta-php@8.0", // should be omitted, invalid
"php@8.0-coolio", // should be omitted, invalid
"php@7.0",
"",
"unrelatedphp@1.0", // should be omitted, invalid
"php@5.6",
"php@5.4" // should be omitted, not supported
], checkBinaries: false, generateHelpers: false)
XCTAssertEqual(outcome, ["8.0", "7.0"])
}
}

View File

@ -1,18 +0,0 @@
//
// ValetTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class ValetVersionExtractorTest: XCTestCase {
func testDetermineValetVersion() {
let version = valet("--version", sudo: false)
XCTAssert(version.contains("Laravel Valet 2") || version.contains("Laravel Valet 3"))
}
}

View File

@ -7,7 +7,7 @@
"alpha" : "1.000",
"blue" : "0.988",
"green" : "0.580",
"red" : "0.277"
"red" : "0.278"
}
},
"idiom" : "universal"

View File

@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.501",
"green" : "0.697",
"red" : "0.247"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.501",
"green" : "0.765",
"red" : "0.247"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.180",
"green" : "0.000",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.426",
"green" : "0.363",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,38 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.180",
"green" : "0.841",
"red" : "1.000"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.426",
"green" : "0.809",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,25 @@
//
// ActiveCommand.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var Command: CommandProtocol {
return ActiveCommand.shared
}
class ActiveCommand {
static var shared: CommandProtocol = RealCommand()
public static func useTestable(_ output: [String: String]) {
Self.shared = TestableCommand(commands: output)
}
public static func useSystem() {
Self.shared = RealCommand()
}
}

View File

@ -0,0 +1,22 @@
//
// CommandProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol CommandProtocol {
/**
Immediately executes a command.
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
*/
func execute(path: String, arguments: [String], trimNewlines: Bool) -> String
}

View File

@ -2,21 +2,14 @@
// Command.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
public class Command {
public class RealCommand: CommandProtocol {
/**
Immediately executes a command.
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
*/
public static func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
public func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
let task = Process()
task.launchPath = path
task.arguments = arguments

View File

@ -2,7 +2,7 @@
// Services.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,36 +12,37 @@ class Actions {
// MARK: - Services
public static func restartPhpFpm() {
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
public static func restartPhpFpm() async {
await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
}
public static func restartNginx() {
brew("services restart nginx", sudo: true)
public static func restartNginx() async {
await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
}
public static func restartDnsMasq() {
brew("services restart dnsmasq", sudo: true)
public static func restartDnsMasq() async {
await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
}
public static func stopValetServices() {
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
public static func stopValetServices() async {
await brew("services stop \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
await brew("services stop \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
await brew("services stop \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
}
public static func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq"
"\(Paths.brew) services stop \(Homebrew.Formulae.nginx)",
"\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)"
]
var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)"
]
PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion
let formula = version == PhpEnv.brewPhpAlias
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)")
@ -53,9 +54,10 @@ class Actions {
+ " && "
+ cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges"
)
let source = "do shell script \"\(script)\" with administrator privileges"
Log.perf(source)
let appleScript = NSAppleScript(source: source)
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
@ -64,29 +66,6 @@ class Actions {
}
}
// MARK: - Third Party Services
public static func stopService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
public static func startService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() {
@ -94,37 +73,33 @@ class Actions {
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder() {
let file = URL(string: "file://~/.composer/composer.json".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openValetConfigFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
let file = URL(string: "file://~/.config/valet".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpMonitorConfigFile() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/phpmon")
let file = URL(string: "file://~/.config/phpmon".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
public static func createTempPhpInfoFile() async -> URL {
try! FileSystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
// Tell php-cgi to run the PHP and output as an .html file
Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
await Shell.quiet("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
}
@ -143,12 +118,10 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyValet(completed: @escaping () -> Void) {
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)
brew("services restart nginx", sudo: true)
completed()
})
public static func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias)
await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
}
}

View File

@ -2,53 +2,54 @@
// Constants.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
struct Constants {
/**
* The latest PHP version that is considered to be stable at the time of release.
* This version number is currently not used (only as a default fallback).
*/
static let LatestStablePhpVersion = "8.1"
/**
The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown
every time the app launches (with a recommendation to upgrade).
The minimum requirement is currently synced to PHP 8.1 compatibility.
See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/
static let MinimumRecommendedValetVersion = "2.16.2"
/**
* The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list.
* Any other PHP versions are considered invalid.
*/
static let SupportedPhpVersions = [
// ====================
// STABLE RELEASES
// ====================
// Versions of PHP that are stable and are supported.
"5.6", // only supported when Valet 2.x is active
"7.0",
"7.1",
"7.2",
"7.3",
"7.4",
"8.0",
"8.1",
static let DetectedPhpVersions: Set = [
"5.6",
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2", "8.3"
]
// ====================
// EXPERIMENTAL SUPPORT
// ====================
// Every release that supports the next release will always support the next
// dev release. In this case, that means that the version below is detected.
"8.2"
/**
The PHP versions supported by each version of Valet.
*/
static let ValetSupportedPhpVersionMatrix: [Int: Set] = [
2: // Valet v2 has the broadest legacy support
[
"5.6",
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2"
],
3: // Valet v3 dropped support for v5.6
[
"7.0", "7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2",
"8.3" // dev
],
4: // Valet v4 dropped support for v7.0
[
"7.1", "7.2", "7.3", "7.4",
"8.0", "8.1", "8.2",
"8.3" // dev
]
]
struct Urls {

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
// MARK: Common Shell Commands
@ -11,41 +11,49 @@
/**
Runs a `valet` command. Defaults to running as superuser.
*/
func valet(_ command: String, sudo: Bool = true) -> String {
return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
func valet(_ command: String, sudo: Bool = true) async -> String {
return await Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)").out
}
/**
Runs a `brew` command. Can run as superuser.
*/
func brew(_ command: String, sudo: Bool = false) {
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
func brew(_ command: String, sudo: Bool = false) async {
await Shell.quiet("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
}
/**
Runs `sed` in order to replace all occurrences of a string in a specific file with another.
*/
func sed(file: String, original: String, replacement: String) {
func sed(file: String, original: String, replacement: String) async {
// Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension
if Filesystem.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
if FileSystem.fileExists("\(Paths.binPath)/gsed") {
await Shell.quiet("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else {
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
await Shell.quiet("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
}
}
/**
Uses `grep` to determine whether a particular query string can be found in a particular file.
*/
func grepContains(file: String, query: String) -> Bool {
return Shell.pipe("""
func grepContains(file: String, query: String) async -> Bool {
return await Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""")
""").out
.trimmingCharacters(in: .whitespacesAndNewlines)
.contains("YES")
}
/**
Attempts to introduce sleep for a particular duration. Use with caution.
Only intended for testing purposes.
*/
func delay(seconds: Double) async {
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
}

View File

@ -0,0 +1,60 @@
//
// Homebrew.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class Homebrew {
static var fake: Bool = false
struct Formulae {
static var php: HomebrewFormula {
if Homebrew.fake {
return HomebrewFormula("php", elevated: true)
}
if PhpEnv.shared.homebrewPackage == nil {
fatalError("You must either load the HomebrewPackage object or call `fake` on the Homebrew class.")
}
return HomebrewFormula(PhpEnv.phpInstall.formula, elevated: true)
}
static var nginx: HomebrewFormula {
return HomebrewDiagnostics.usesNginxFullFormula
? HomebrewFormula("nginx-full", elevated: true)
: HomebrewFormula("nginx", elevated: true)
}
static var dnsmasq: HomebrewFormula {
return HomebrewFormula("dnsmasq", elevated: true)
}
}
}
class HomebrewFormula: Equatable, Hashable, CustomStringConvertible {
let name: String
let elevated: Bool
var description: String {
return name
}
init(_ name: String, elevated: Bool = true) {
self.name = name
self.elevated = elevated
}
static func == (lhs: HomebrewFormula, rhs: HomebrewFormula) -> Bool {
return lhs.elevated == rhs.elevated && lhs.name == rhs.name
}
public func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(elevated)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,47 +12,76 @@ class Log {
static var shared = Log()
var logFilePath = "~/.config/phpmon/last_session.log"
var logExists = false
enum Verbosity: Int {
case error = 1,
warning = 2,
info = 3,
performance = 4
performance = 4,
cli = 5
public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue
}
}
var verbosity: Verbosity = .warning
public func prepareLogFile() {
if !isRunningTests && Verbosity.cli.isApplicable() {
_ = system("mkdir -p ~/.config/phpmon 2> /dev/null")
_ = system("rm ~/.config/phpmon/last_session.log 2> /dev/null")
_ = system("touch ~/.config/phpmon/last_session.log 2> /dev/null")
self.logExists = FileSystem.fileExists(self.logFilePath)
}
}
var verbosity: Verbosity = .warning {
didSet {
self.prepareLogFile()
}
}
static func err(_ item: Any) {
if Verbosity.error.isApplicable() {
print("[E] \(item)")
Log.shared.log("[E] \(item)")
}
}
static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() {
print("[W] \(item)")
Log.shared.log("[W] \(item)")
}
}
static func info(_ item: Any) {
if Verbosity.info.isApplicable() {
print("\(item)")
Log.shared.log("\(item)")
}
}
static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() {
print("[P] \(item)")
Log.shared.log("[P] \(item)")
}
}
static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() {
print("==================================")
Log.shared.log("==================================")
}
}
private func log(_ text: String) {
print(text)
if logExists && Verbosity.cli.isApplicable() {
let logFile = URL(string: self.logFilePath.replacingTildeWithHomeDirectory)!
if let fileHandle = try? FileHandle(forWritingTo: logFile) {
fileHandle.seekToEndOfFile()
fileHandle.write(text.appending("\n").data(using: .utf8).unsafelyUnwrapped)
fileHandle.closeFile()
}
}
}
}

View File

@ -2,7 +2,7 @@
// Paths.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -16,12 +16,12 @@ public class Paths {
public static let shared = Paths()
internal var baseDir: Paths.HomebrewDir
private var userName: String
init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("id -un").split(separator: "\n")[0])
userName = identity()
Log.info("[ID] The current username is `\(userName)`.")
}
public func detectBinaryPaths() {
@ -58,7 +58,16 @@ public class Paths {
}
public static var homePath: String {
return NSHomeDirectory()
if FileSystem is RealFileSystem {
return NSHomeDirectory()
}
if FileSystem is TestableFileSystem {
let fs = FileSystem as! TestableFileSystem
return fs.homeDirectory
}
fatalError("A valid FileSystem must be allowed to return the home path")
}
public static var cellarPath: String {
@ -82,9 +91,9 @@ public class Paths {
// (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() {
if Filesystem.fileExists("/usr/local/bin/composer") {
if FileSystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer"
} else if Filesystem.fileExists("/opt/homebrew/bin/composer") {
} else if FileSystem.fileExists("/opt/homebrew/bin/composer") {
Paths.composer = "/opt/homebrew/bin/composer"
} else {
Paths.composer = nil

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 23/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -1,33 +0,0 @@
//
// Shell+PATH.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Shell {
var PATH: String {
let task = Process()
task.launchPath = "/bin/zsh"
let command = Filesystem.fileExists("~/.zshrc")
// source the user's .zshrc file if it exists to complete $PATH
? ". ~/.zshrc && echo $PATH"
// otherwise, non-interactive mode is sufficient
: "echo $PATH"
task.arguments = ["--login", "-lc", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
return String(data: data, encoding: String.Encoding.utf8) ?? ""
}
}

View File

@ -1,171 +0,0 @@
//
// Shell.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
public class Shell {
// MARK: - Invoke static functions
public static func run(
_ command: String,
requiresPath: Bool = false
) {
Shell.user.run(command, requiresPath: requiresPath)
}
public static func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath)
}
// MARK: - Singleton
/**
We now require macOS 11, so no need to detect which terminal to use.
*/
public var shell: String = "/bin/sh"
/** Additional exports that are sent if `requiresPath` is set to true. */
public var exports: String = ""
/**
Singleton to access a user shell (with --login)
*/
public static let user = Shell()
/**
Runs a shell command without using the output.
Uses the default shell.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func run(
_ command: String,
requiresPath: Bool = false
) {
// Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath)
}
/**
Runs a shell command and returns the output.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
*/
private func pipe(
_ command: String,
requiresPath: Bool = false
) -> String {
let shellOutput = self.executeSynchronously(command, requiresPath: requiresPath)
let hasError = (
shellOutput.standardOutput == ""
&& shellOutput.errorOutput.lengthOfBytes(using: .utf8) > 0
)
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
}
/**
Runs the command and returns a `ShellOutput` object, which contains info about the process.
- Parameter command: The command to run
- Parameter requiresPath: By default, the PATH is not resolved but some binaries might require this
- Parameter waitUntilExit: Waits for the command to complete before returning the `ShellOutput`
*/
public func executeSynchronously(
_ command: String,
requiresPath: Bool = false
) -> Shell.Output {
let outputPipe = Pipe()
let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
let output = Shell.Output(
standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
errorOutput: String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!,
task: task
)
if CommandLine.arguments.contains("--v") {
log(task: task, output: output)
}
return output
}
/**
Creates a new process with the correct PATH and shell.
*/
public func createTask(for command: String, requiresPath: Bool) -> Process {
var completeCommand = ""
if requiresPath {
// Basic export (PATH)
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
// Put additional exports in between
if !self.exports.isEmpty {
completeCommand += "\(self.exports) && "
}
}
completeCommand += command
let task = Process()
task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
return task
}
/**
Verbose logging for PHP Monitor's synchronous shell output.
*/
private func log(task: Process, output: Output) {
Log.info("")
Log.info("==== COMMAND ====")
Log.info("")
Log.info("\(self.shell) \(task.arguments?.joined(separator: " ") ?? "")")
Log.info("")
Log.info("==== OUTPUT ====")
Log.info("")
dump(output)
Log.info("")
Log.info("==== END OUTPUT ====")
Log.info("")
}
public class Output {
public let standardOutput: String
public let errorOutput: String
public let task: Process
init(standardOutput: String,
errorOutput: String,
task: Process) {
self.standardOutput = standardOutput
self.errorOutput = errorOutput
self.task = task
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 06/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 08/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -0,0 +1,21 @@
//
// DataExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Data {
var prettyPrintedJSONString: NSString? {
guard let object = try? JSONSerialization.jsonObject(with: self, options: []),
let data = try? JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted]),
let prettyPrintedString = NSString(data: data, encoding: String.Encoding.utf8.rawValue) else {
return nil
}
return prettyPrintedString
}
}

View File

@ -2,7 +2,7 @@
// Date.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -0,0 +1,17 @@
//
// DictionaryExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Dictionary {
mutating func renameKey(fromKey: Key, toKey: Key) {
if let entry = removeValue(forKey: fromKey) {
self[toKey] = entry
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 18/08/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 17/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -2,20 +2,43 @@
// StringExtension.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
struct Localization {
static var bundle: Bundle = {
if !isRunningTests {
return Bundle.main
}
let foundBundle = Bundle(identifier: "com.nicoverbruggen.phpmon.dev")
?? Bundle(identifier: "com.nicoverbruggen.phpmon")
?? Bundle(identifier: "com.nicoverbruggen.phpmon.ui-tests")
if foundBundle == nil {
let bundles = Bundle.allBundles
.map { $0.bundleIdentifier }
.filter { $0 != nil }
.map { $0! }
fatalError("The following bundles were found: \(bundles)")
}
return foundBundle!
}()
}
extension String {
var localized: String {
if #available(macOS 13, *) {
return NSLocalizedString(
self, tableName: nil, bundle: Bundle.main, value: "", comment: ""
self, tableName: nil, bundle: Localization.bundle, value: "", comment: ""
).replacingOccurrences(of: "Preferences", with: "Settings")
}
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
return NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
}
var localizedForSwiftUI: LocalizedStringKey {
@ -42,6 +65,16 @@ extension String {
return count
}
func matches(pattern: String) -> Bool {
let pred = NSPredicate(format: "self LIKE %@", pattern)
return !NSArray(object: self).filtered(using: pred).isEmpty
}
static func random(_ length: Int) -> String {
let characters = "0123456789abcdefghijklmnopqrstuvwxyz"
return String((0..<length).map { _ in characters.randomElement()! })
}
subscript(r: Range<String.Index>) -> String {
let start = r.lowerBound
let end = r.upperBound
@ -98,5 +131,4 @@ extension String {
return ""
}
}
}

View File

@ -0,0 +1,15 @@
//
// TimeExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 29/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension TimeInterval {
public static func minutes(_ amount: Int) -> TimeInterval {
return Double(amount * 60)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -0,0 +1,26 @@
//
// FS.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var FileSystem: FileSystemProtocol {
return ActiveFileSystem.shared
}
class ActiveFileSystem {
static var shared: FileSystemProtocol = RealFileSystem()
/** Note: Intermediate directories are not automatically inferred and have to be manually declared. */
public static func useTestable(_ files: [String: FakeFile]) {
Self.shared = TestableFileSystem(files: files)
}
public static func useSystem() {
Self.shared = RealFileSystem()
}
}

View File

@ -0,0 +1,50 @@
//
// FileSystemProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol FileSystemProtocol {
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws
func writeAtomicallyToFile(_ path: String, content: String) throws
func getStringFromFile(_ path: String) throws -> String
func getShallowContentsOfDirectory(_ path: String) throws -> [String]
func getDestinationOfSymlink(_ path: String) throws -> String
// MARK: - Move & Delete Files
func move(from path: String, to newPath: String) throws
func remove(_ path: String) throws
// MARK: Attributes
func makeExecutable(_ path: String) throws
// MARK: - Checks
func isExecutableFile(_ path: String) -> Bool
func isWriteableFile(_ path: String) -> Bool
func anyExists(_ path: String) -> Bool
func fileExists(_ path: String) -> Bool
func directoryExists(_ path: String) -> Bool
func isSymlink(_ path: String) -> Bool
func isDirectory(_ path: String) -> Bool
}

View File

@ -0,0 +1,129 @@
//
// RealFileSystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 08/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension String {
var replacingTildeWithHomeDirectory: String {
return self.replacingOccurrences(of: "~", with: Paths.homePath)
}
}
class RealFileSystem: FileSystemProtocol {
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) {
try! FileManager.default.createDirectory(
atPath: path.replacingTildeWithHomeDirectory,
withIntermediateDirectories: withIntermediateDirectories
)
}
func writeAtomicallyToFile(_ path: String, content: String) throws {
try content.write(
to: URL(fileURLWithPath: path.replacingTildeWithHomeDirectory),
atomically: true,
encoding: String.Encoding.utf8
)
}
func getStringFromFile(_ path: String) throws -> String {
return try String(
contentsOf: URL(fileURLWithPath: path.replacingTildeWithHomeDirectory),
encoding: .utf8
)
}
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
return try FileManager.default.contentsOfDirectory(atPath: path.replacingTildeWithHomeDirectory)
}
func getDestinationOfSymlink(_ path: String) throws -> String {
return try FileManager.default.destinationOfSymbolicLink(atPath: path.replacingTildeWithHomeDirectory)
}
// MARK: - Move & Delete Files
func move(from path: String, to newPath: String) throws {
try FileManager.default.moveItem(
atPath: path.replacingTildeWithHomeDirectory,
toPath: newPath.replacingTildeWithHomeDirectory
)
}
func remove(_ path: String) throws {
try FileManager.default.removeItem(atPath: path.replacingTildeWithHomeDirectory)
}
// MARK: FS Attributes
func makeExecutable(_ path: String) throws {
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
}
// MARK: - Checks
func isExecutableFile(_ path: String) -> Bool {
return FileManager.default.isExecutableFile(
atPath: path.replacingTildeWithHomeDirectory
) && FileManager.default.isReadableFile(
atPath: path.replacingTildeWithHomeDirectory
)
}
func isWriteableFile(_ path: String) -> Bool {
return FileManager.default.isWritableFile(
atPath: path.replacingTildeWithHomeDirectory
)
}
func anyExists(_ path: String) -> Bool {
return FileManager.default.fileExists(
atPath: path.replacingTildeWithHomeDirectory
)
}
func fileExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingTildeWithHomeDirectory,
isDirectory: &isDirectory
)
return exists && !isDirectory.boolValue
}
func directoryExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingTildeWithHomeDirectory,
isDirectory: &isDirectory
)
return exists && isDirectory.boolValue
}
func isSymlink(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
} catch {
return false
}
}
func isDirectory(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeDirectory
} catch {
return false
}
}
}

View File

@ -2,7 +2,7 @@
// Alert.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -13,8 +13,8 @@ class Alert {
onWindow window: NSWindow,
messageText: String,
informativeText: String,
buttonTitle: String = "OK",
secondButtonTitle: String = "Cancel",
buttonTitle: String = "generic.ok".localized,
secondButtonTitle: String = "generic.cancel".localized,
style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void)
) {

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -34,30 +34,45 @@ class Application {
(This will open the app if it isn't open yet.)
*/
@objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
Task { await Shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"") }
}
/** Checks if the app is installed. */
func isInstalled() -> Bool {
// If this script does not complain, the app exists!
return Shell.user.executeSynchronously(
func isInstalled() async -> Bool {
let (process, output) = try! await Shell.attach(
"/usr/bin/open -Ra \"\(name)\"",
requiresPath: false
).task.terminationStatus == 0
didReceiveOutput: { _, _ in },
withTimeout: 2.0
)
if Shell is TestableShell {
// When testing, check the error output (must not be empty)
return !output.hasError
} else {
// If this script does not complain, the app exists!
return process.terminationStatus == 0
}
}
/**
Detect which apps are available to open a specific directory.
*/
static public func detectPresetApplications() -> [Application] {
return [
static public func detectPresetApplications() async -> [Application] {
var detected: [Application] = []
let detectable = [
Application("PhpStorm", .editor),
Application("Visual Studio Code", .editor),
Application("Sublime Text", .editor),
Application("Sublime Merge", .git_gui),
Application("iTerm", .terminal)
].filter {
return $0.isInstalled()
]
for app in detectable where await app.isInstalled() {
detected.append(app)
}
return detected
}
}

View File

@ -1,61 +0,0 @@
//
// Filesystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Cocoa
import Foundation
class Filesystem {
/**
Checks if a file or directory exists at the provided path.
*/
public static func exists(_ path: String) -> Bool {
return FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath)
)
}
/**
Checks if a file exists at the provided path.
*/
public static func fileExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
isDirectory: &isDirectory
)
return exists && !isDirectory.boolValue
}
/**
Checks if a directory exists at the provided path.
*/
public static func directoryExists(_ path: String) -> Bool {
var isDirectory: ObjCBool = true
let exists = FileManager.default.fileExists(
atPath: path.replacingOccurrences(of: "~", with: Paths.homePath),
isDirectory: &isDirectory
)
return exists && isDirectory.boolValue
}
/**
Checks if a given file is a symbolic link.
*/
public static func fileIsSymlink(_ path: String) -> Bool {
do {
let attribs = try FileManager.default.attributesOfItem(atPath: path)
return attribs[.type] as! FileAttributeType == FileAttributeType.typeSymbolicLink
} catch {
return false
}
}
}

View File

@ -2,7 +2,7 @@
// LocalNotification.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -10,8 +10,8 @@ import UserNotifications
class LocalNotification {
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName) {
if !Preferences.isEnabled(preference) {
@MainActor public static func send(title: String, subtitle: String, preference: PreferenceName?) {
if preference != nil && !Preferences.isEnabled(preference!) {
return
}

View File

@ -0,0 +1,25 @@
//
// LoginItemManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 15/02/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import AppKit
import ServiceManagement
@available(macOS 13.0, *)
class LoginItemManager {
func loginItemIsEnabled() -> Bool {
return SMAppService.mainApp.status == .enabled
}
func disableLoginItem() {
try? SMAppService.mainApp.unregister()
}
func enableLoginItem() {
try? SMAppService.mainApp.register()
}
}

View File

@ -2,7 +2,7 @@
// ImageGenerator.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -30,7 +30,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
}
deinit {
Log.perf("Window controller '\(windowName)' was deinitialized")
Log.perf("deinit: \(String(describing: self)).\(#function)")
}
}

View File

@ -0,0 +1,56 @@
//
// System.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
Run a simple blocking Shell command on the user's own system.
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
*/
public func system(_ command: String) -> String {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}
/** Same as the `system` command, but does not return the output. */
public func system_quiet(_ command: String) {
_ = system(command)
}
/**
Retrieves the username for the currently signed in user via `/usr/bin/id`.
This cannot fail or the application will crash.
*/
public func identity() -> String {
let task = Process()
task.launchPath = "/usr/bin/id"
task.arguments = ["-un"]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
guard let output = String(
data: pipe.fileHandleForReading.readDataToEndOfFile(),
encoding: String.Encoding.utf8
) else {
fatalError("Could not retrieve username via `id -un`!")
}
return output.trimmingCharacters(in: .whitespacesAndNewlines)
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -40,5 +40,4 @@ class VersionExtractor {
return nil
}
}
}

View File

@ -0,0 +1,17 @@
//
// WIP.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
func todo(_ context: String = "") {
if !context.isEmpty {
fatalError("To be implemented: \(context)")
}
fatalError("To be implemented")
}

View File

@ -2,7 +2,7 @@
// ActivePhpInstallation.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -17,11 +17,12 @@ import Foundation
Using `version.short` is advisable if you want to interact with Homebrew.
*/
class ActivePhpInstallation {
var version: Version!
var version: VersionNumber!
var limits: Limits!
var iniFiles: [PhpConfigurationFile] = []
var hasErrorState: Bool = false
var extensions: [PhpExtension] {
return iniFiles.flatMap { initFile in
return initFile.extensions
@ -31,20 +32,25 @@ class ActivePhpInstallation {
// MARK: - Computed
var formula: String {
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
return (version.short == PhpEnv.brewPhpAlias) ? "php" : "php@\(version.short)"
}
// MARK: - Initializer
init() {
// Show information about the current version
getVersion()
do {
try determineVersion()
} catch {
// TODO: In future versions of PHP Monitor, this should not crash
fatalError("Could not determine or parse PHP version; aborting")
}
// Initialize the list of ini files that are loaded
iniFiles = []
// If an error occurred, exit early
if version.error {
if self.hasErrorState {
limits = Limits()
return
}
@ -64,10 +70,14 @@ class ActivePhpInstallation {
)
// Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
let paths = Command.execute(
path: Paths.php,
arguments: ["-r", "echo php_ini_scanned_files();"],
trimNewlines: false
)
.replacingOccurrences(of: "\n", with: "")
.split(separator: ",")
.map { String($0) }
// See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in
@ -81,26 +91,12 @@ class ActivePhpInstallation {
When the app tries to retrieve the version, the installation is considered broken if the output is nothing,
_or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/
private func getVersion() {
self.version = Version()
private func determineVersion() throws {
let output = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
self.hasErrorState = (output == "" || output.contains("Warning") || output.contains("Error"))
if version == "" || version.contains("Warning") || version.contains("Error") {
self.version.short = "💩 BROKEN"
self.version.long = ""
self.version.error = true
return
}
// That's the long version
self.version.long = version
// Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements
self.version.short = segments[0...1].joined(separator: ".")
self.version = try? VersionNumber.parse(output)
}
/**
@ -119,7 +115,7 @@ class ActivePhpInstallation {
- Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`.
*/
private func getByteCount(key: String) -> String {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"])
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"], trimNewlines: false)
// Check if the value is unlimited
if value == "-1" {
@ -139,31 +135,20 @@ class ActivePhpInstallation {
versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails,
that means that Valet won't work properly.
*/
func checkPhpFpmStatus() -> Bool {
func checkPhpFpmStatus() async -> Bool {
if self.version.short == "5.6" {
// The main PHP config file should contain `valet.sock` and then we're probably fine?
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock")
return await Shell.pipe("cat \(fileName)").out
.contains("valet.sock")
}
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Filesystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
return FileSystem.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf")
}
// MARK: - Structs
/**
Struct containing information about the version number of the current PHP installation.
Also includes information about whether the install is considered "broken" or not.
If an error was found in the terminal output, `error` is set to `true` and the installation
can be considered broken. (The app will display this as well.)
*/
struct Version {
var short = "???"
var long = "???"
var error = false
}
/**
Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size.

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 01/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -2,14 +2,12 @@
// HomebrewPackage.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
struct HomebrewPackage: Decodable {
let name: String
let full_name: String
let aliases: [String]
let installed: [HomebrewInstalled]

View File

@ -3,12 +3,12 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 11/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
struct HomebrewService: Decodable, Equatable {
final class HomebrewService: Sendable, Decodable {
let name: String
let service_name: String
let running: Bool
@ -19,10 +19,32 @@ struct HomebrewService: Decodable, Equatable {
let log_path: String?
let error_log_path: String?
init(
name: String,
service_name: String,
running: Bool,
loaded: Bool,
pid: Int? = nil,
user: String? = nil,
status: String? = nil,
log_path: String? = nil,
error_log_path: String? = nil
) {
self.name = name
self.service_name = service_name
self.running = running
self.loaded = loaded
self.pid = pid
self.user = user
self.status = status
self.log_path = log_path
self.error_log_path = error_log_path
}
/**
Dummy data for preview purposes.
*/
public static func dummy(named service: String, enabled: Bool) -> Self {
public static func dummy(named service: String, enabled: Bool, status: String? = nil) -> HomebrewService {
return HomebrewService(
name: service,
service_name: service,
@ -30,7 +52,7 @@ struct HomebrewService: Decodable, Equatable {
loaded: enabled,
pid: nil,
user: nil,
status: nil,
status: status,
log_path: nil,
error_log_path: nil
)

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -14,15 +14,17 @@ class PhpEnv {
init() {
self.currentInstall = ActivePhpInstallation()
}
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
func determinePhpAlias() async {
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
Log.info("[BREW] On your system, the `php` formula means version \(homebrewPackage.version)!")
}
// MARK: - Properties
@ -36,14 +38,17 @@ class PhpEnv {
/** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false
/** All available versions of PHP. */
/** All versions of PHP that are currently supported. */
var availablePhpVersions: [String] = []
/** All versions of PHP that are currently installed but not compatible. */
var incompatiblePhpVersions: [String] = []
/** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation
var currentInstall: ActivePhpInstallation!
/**
The version that the `php` formula via Brew is aliased to on the current system.
@ -54,7 +59,9 @@ class PhpEnv {
As such, we take that information from Homebrew.
*/
static var brewPhpVersion: String {
static var brewPhpAlias: String {
if Homebrew.fake { return "8.2" }
return Self.shared.homebrewPackage.version
}
@ -76,17 +83,21 @@ class PhpEnv {
return InternalSwitcher()
}
public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions()
public static func detectPhpVersions() async {
_ = await Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
*/
public func detectPhpVersions() -> [String] {
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
public func detectPhpVersions() async -> Set<String> {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
let versions = await extractPhpVersions(from: files.components(separatedBy: "\n"))
let supportedByValet = Constants.ValetSupportedPhpVersionMatrix[Valet.shared.version.major] ?? []
var supportedVersions = versions.intersection(supportedByValet)
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
@ -94,13 +105,18 @@ class PhpEnv {
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
versionsOnly.append(phpAlias)
if !supportedVersions.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
supportedVersions.insert(phpAlias)
}
Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = Array(supportedVersions)
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
availablePhpVersions = versionsOnly
incompatiblePhpVersions = Array(versions.subtracting(supportedByValet))
.sorted(by: { $0.versionCompare($1) == .orderedDescending })
Log.info("The PHP versions that were detected are: \(availablePhpVersions)")
Log.info("The PHP versions that were unsupported are: \(incompatiblePhpVersions)")
var mappedVersions: [String: PhpInstallation] = [:]
@ -110,7 +126,7 @@ class PhpEnv {
cachedPhpInstallations = mappedVersions
return versionsOnly
return supportedVersions
}
/**
@ -124,15 +140,9 @@ class PhpEnv {
from versions: [String],
checkBinaries: Bool = true,
generateHelpers: Bool = true
) -> [String] {
var output: [String] = []
var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" }
}
) async -> Set<String> {
let supported = Constants.DetectedPhpVersions
var output: Set<String> = []
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
@ -143,19 +153,21 @@ class PhpEnv {
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& supported.contains(version)
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
output.append(version)
&& (checkBinaries ? FileSystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
output.insert(version)
}
}
if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) }
for item in output {
await PhpHelper.generate(for: item)
}
}
return output
}
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
public func validVersions(for constraint: String) -> [VersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
.make(from: self.availablePhpVersions)
@ -169,6 +181,9 @@ class PhpEnv {
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
Log.info("Keeping track that this is the new version!")
Stats.persistCurrentGlobalPhpVersion(version: version)
return true
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 17/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -12,7 +12,7 @@ class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) {
public static func generate(for version: String) async {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
@ -20,79 +20,82 @@ class PhpHelper {
let destination = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
// Check if the ~/.config/phpmon/bin directory is in the PATH
let inPath = Shell.user.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
let inPath = Shell.PATH.contains("\(Paths.homePath)/.config/phpmon/bin")
// Check if we can create symlinks (`/usr/local/bin` must be writable)
let canWriteSymlinks = FileManager.default.isWritableFile(atPath: "/usr/local/bin/")
let canWriteSymlinks = FileSystem.isWriteableFile("/usr/local/bin/")
do {
Shell.run("mkdir -p ~/.config/phpmon/bin")
if FileManager.default.fileExists(atPath: destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.")
return
}
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
// Write to the destination
try script.write(
to: URL(fileURLWithPath: destination),
atomically: true,
encoding: String.Encoding.utf8
)
// Make sure the file is executable
Shell.run("chmod +x \(destination)")
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
Task { // Create the appropriate folders and check if the files exist
do {
if !FileSystem.directoryExists("~/.config/phpmon/bin") {
try FileSystem.createDirectory(
"~/.config/phpmon/bin",
withIntermediateDirectories: true
)
}
// Write the symlink
self.createSymlink(dotless)
if FileSystem.fileExists(destination) {
let contents = try String(contentsOfFile: destination)
if !contents.contains(keyPhrase) {
Log.info("The file at '\(destination)' already exists and was not generated by PHP Monitor "
+ "(or is unreadable). Not updating this file.")
return
}
}
// Let's follow the symlink to the PHP binary folder
let path = URL(fileURLWithPath: "\(Paths.optPath)/php@\(version)/bin")
.resolvingSymlinksInPath().path
// The contents of the script!
let script = """
#!/bin/zsh
# \(keyPhrase)
# It reflects the location of PHP \(version)'s binaries on your system.
# Usage: . pm\(dotless)
[[ $ZSH_EVAL_CONTEXT =~ :file$ ]] \\
&& echo "PHP Monitor has enabled this terminal to use PHP \(version)." \\
|| echo "You must run '. pm\(dotless)' (or 'source pm\(dotless)') instead!";
export PATH=\(path):$PATH
"""
try FileSystem.writeAtomicallyToFile(destination, content: script)
if !FileSystem.isExecutableFile(destination) {
try FileSystem.makeExecutable(destination)
}
// Create a symlink if the folder is not in the PATH
if !inPath {
// First, check if we can create symlinks at all
if !canWriteSymlinks {
Log.err("PHP Monitor does not have permission to symlink `/usr/local/bin/\(dotless)`.")
return
}
// Write the symlink
await self.createSymlink(dotless)
}
} catch {
Log.err(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
}
} catch {
print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to \(destination))")
}
}
private static func createSymlink(_ dotless: String) {
private static func createSymlink(_ dotless: String) async {
let source = "\(Paths.homePath)/.config/phpmon/bin/pm\(dotless)"
let destination = "/usr/local/bin/pm\(dotless)"
if !Filesystem.fileExists(destination) {
if !FileSystem.fileExists(destination) {
Log.info("Creating new symlink: \(destination)")
Shell.run("ln -s \(source) \(destination)")
await Shell.quiet("ln -s \(source) \(destination)")
return
}
if !Filesystem.fileIsSymlink(destination) {
if !FileSystem.isSymlink(destination) {
Log.info("Overwriting existing file with new symlink: \(destination)")
Shell.run("ln -fs \(source) \(destination)")
await Shell.quiet("ln -fs \(source) \(destination)")
return
}

View File

@ -1,208 +0,0 @@
//
// PhpVersionNumber.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { try! PhpVersionNumber.parse($0) }
)
}
public var first: PhpVersionNumber? {
return self.versions.first
}
public var all: [PhpVersionNumber] {
return self.versions
}
/**
Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained.
More information on these constraints can be found here:
https://getcomposer.org/doc/articles/versions.md#writing-version-constraints
- Parameter constraint: The full constraint as a string (e.g. "^7.0")
- Parameter strict: Whether the patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
assumes that the patch version is .999, which means that in all cases the patch version check is
always going to pass.
**STRICT MODE (= patch precision on)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
When checking against actual PHP versions installed by the user (with patch precision), use
strict mode.
**NON-STRICT MODE (= patch precision off)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
*/
public func matching(constraint: String, strict: Bool = false) -> [PhpVersionNumber] {
if let version = PhpVersionNumber.make(from: constraint, type: .versionOnly) {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
return self.versions.filter {
version.patch != nil
// If a patch is provided then the minor version cannot be bumped.
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
// If a patch is not provided then the major version cannot be bumped.
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
}
if let version = PhpVersionNumber.make(from: constraint, type: .smallerThan) {
return self.versions.filter { $0.isOlderThan(version, strict)}
}
return []
}
}
public struct PhpVersionNumber: Equatable, Hashable {
let major: Int
let minor: Int
let patch: Int?
public func toString() -> String {
return self.patch == nil
? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)"
}
public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
public var homebrewVersion: String {
return "\(major).\(minor)"
}
public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
}
public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError()
}
return Self.make(from: versionText)!
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil {
let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!]
)!
let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)!
var patch: Int?
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange])
}
return Self(major: major, minor: minor, patch: patch)
}
return nil
}
// MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
self.major == version.major && self.minor > version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) > version.patch(strict)
)
}
internal func isOlderThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return (
self.major < version.major ||
self.major == version.major && self.minor < version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) < version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor
}
}

View File

@ -0,0 +1,117 @@
//
// PhpVersionNumberCollection.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 06/01/2023.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct PhpVersionNumberCollection: Equatable {
let versions: [VersionNumber]
public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection(
versions: versions.map { try! VersionNumber.parse($0) }
)
}
public var first: VersionNumber? {
return self.versions.first
}
public var all: [VersionNumber] {
return self.versions
}
/**
Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained.
More information on these constraints can be found here:
https://getcomposer.org/doc/articles/versions.md#writing-version-constraints
- Parameter constraint: The full constraint as a string (e.g. "^7.0")
- Parameter strict: Whether the patch version check is strict. See more below.
The strict mode does not matter if a patch version is provided for all versions in the collection.
It also does not matter for certain comparisons (e.g. when dealing with wildcards).
Strict mode assumes that any PHP version lacking precise patch information, e.g. inferred
from Homebrew corresponds to the .0 patch version of that version. The default, which is imprecise,
assumes that the patch version is .999, which means that in all cases the patch version check is
always going to pass.
**STRICT MODE (= patch precision on)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in strict mode only 8.1.? will
be considered valid (8.0 translates to 8.0.0 and as such is older than 8.0.1, 8.1.0 is OK).
When checking against actual PHP versions installed by the user (with patch precision), use
strict mode.
**NON-STRICT MODE (= patch precision off)**
Given versions 8.0.? and 8.1.?, but the requirement is ^8.0.1, in non-strict mode version 8.0
is assumed to be equal to version 8.0.999, which is actually fine if 8.0.1 is the required version.
In non-strict mode, the patch version is ignored for regular version checks (no caret / tilde).
If checking compatibility with general Homebrew versions of PHP, do NOT use strict mode, since
the patch version there is not used. (The formula php@8.0 suffices for ^8.0.1.)
*/
public func matching(constraint: String, strict: Bool = false) -> [VersionNumber] {
if constraint == "*" {
return self.versions
}
if let version = VersionNumber.make(from: constraint, type: .wildCardPatch) {
// Wildcard for patch (e.g. "7.4.*") must match major and minor (any patch)
return self.versions.filter { $0.hasSameMajorAndMinor(version) }
}
if let version = VersionNumber.make(from: constraint, type: .wildCardMinor) {
// Strict constraint (e.g. "7.*") -> must only match major (any patch, minor)
return self.versions.filter { $0.isSameMajorVersionAs(version) }
}
if let version = VersionNumber.make(from: constraint, type: .versionOnly) {
// Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .caretVersionRange) {
// Caret range means that the major version is never higher but minor version can be higher
// ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis.
return self.versions.filter {
version.patch != nil
// If a patch is provided then the minor version cannot be bumped.
? $0.hasSameMajorAndMinorButNewerOrSamePatch(version, strict)
// If a patch is not provided then the major version cannot be bumped.
: $0.hasSameMajorButNewerOrSameMinor(version, strict)
}
}
if let version = VersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) }
}
if let version = VersionNumber.make(from: constraint, type: .smallerThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isOlderThan(version, strict)}
}
if let version = VersionNumber.make(from: constraint, type: .smallerThan) {
return self.versions.filter { $0.isOlderThan(version, strict)}
}
return []
}
}

View File

@ -0,0 +1,141 @@
//
// PhpVersionNumber.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
A version number that is (mostly) compatible with the semantic versioning standard.
For more information about semantic versioning, see: https://semver.org/
- Note: If you want to check version constraints for PHP versions, please see `PhpVersionNumberCollection`.
*/
public struct VersionNumber: Equatable, Hashable {
let major: Int
let minor: Int
let patch: Int?
var text: String {
return self.patch == nil
? "\(major).\(minor)"
: "\(major).\(minor).\(patch!)"
}
public func patch(_ strictFallback: Bool = true, _ constraint: VersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
}
public var long: String {
return "\(major).\(minor).\(patch ?? 0)"
}
public var short: String {
return "\(major).\(minor)"
}
public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case wildCardPatch = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\*)?\z"#
case wildCardMinor = #"^(?<major>\d+).(?<minor>\*)?\z"#
case caretVersionRange = #"^\^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThanOrEqual = #"^<=(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
}
public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError()
}
return Self.make(from: versionText)!
}
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
guard let match else { return nil }
let major = Int(versionString[Range(match.range(withName: "major"), in: versionString)!])!
var minor: Int = 0
var patch: Int?
if let minorRange = Range(match.range(withName: "minor"), in: versionString) {
let value = versionString[minorRange] as String
// Zero is the fallback if a wildcard was used
minor = Int(value) ?? 0
}
if let patchRange = Range(match.range(withName: "patch"), in: versionString) {
let value = versionString[patchRange] as String
// nil is the fallback if a wildcard was used
patch = Int(value) ?? nil
}
return Self(major: major, minor: minor, patch: patch)
}
// MARK: Comparison Logic
internal func isSameMajorVersionAs(_ version: VersionNumber) -> Bool {
return self.major == version.major
}
internal func isSameAs(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true)
}
internal func hasSameMajorAndMinor(_ version: VersionNumber) -> Bool {
return self.major == version.major && self.minor == version.minor
}
internal func isNewerThan(_ version: VersionNumber, _ strict: Bool) -> Bool {
return (
self.major > version.major ||
self.major == version.major && self.minor > version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) > version.patch(strict)
)
}
internal func isOlderThan(_ version: VersionNumber, _ strict: Bool) -> Bool {
return (
self.major < version.major ||
self.major == version.major && self.minor < version.minor ||
self.major == version.major && self.minor == version.minor
&& self.patch(strict) < version.patch(strict)
)
}
internal func hasNewerMinorVersionOrPatch(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major &&
(
(self.minor == version.minor && self.patch(strict) >= version.patch(strict, self))
|| self.minor > version.minor
)
}
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict)
}
internal func hasSameMajorButNewerOrSameMinor(_ version: VersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major
&& self.minor >= version.minor
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 04/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -35,7 +35,7 @@ class PhpConfigurationFile: CreatedFromFile {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
do {
let fileContents = try String(contentsOfFile: path)
let fileContents = try FileSystem.getStringFromFile(path)
return Self.init(path: path, contents: fileContents)
} catch {
Log.warn("Could not read the PHP configuration file at: `\(filePath)`")

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 31/01/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -75,16 +75,23 @@ class PhpExtension {
This simply toggles the extension in the .ini file.
You may need to restart the other services in order for this change to apply.
*/
func toggle() {
func toggle() async {
let newLine = enabled
// DISABLED: Commented out line
? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "")
sed(file: file, original: line, replacement: newLine)
await sed(file: file, original: line, replacement: newLine)
enabled.toggle()
if !isRunningTests {
Task { @MainActor in
MainMenu.shared.rebuild()
}
}
}
// MARK: - Static Methods

View File

@ -3,14 +3,14 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpInstallation {
var versionNumber: PhpVersionNumber
var versionNumber: VersionNumber
/**
In order to determine details about a PHP installation, well simply run `php-config --version`
@ -19,17 +19,18 @@ class PhpInstallation {
init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.versionNumber = PhpVersionNumber.make(from: version)!
self.versionNumber = VersionNumber.make(from: version)!
if Filesystem.fileExists(phpConfigExecutablePath) {
if FileSystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute(
path: phpConfigExecutablePath,
arguments: ["--version"]
arguments: ["--version"],
trimNewlines: false
).trimmingCharacters(in: .whitespacesAndNewlines)
// The parser should always work, or the string has to be very unusual.
// If so, the app SHOULD crash, so that the users report what's up.
self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
self.versionNumber = try! VersionNumber.parse(longVersionString)
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -15,49 +15,49 @@ class InternalSwitcher: PhpSwitcher {
- unlinking the current version
- stopping the active services
- linking the new desired version
Please note that depending on which version is installed,
the version that is switched to may or may not be identical to `php`
(without @version).
*/
func performSwitch(to version: String, completion: @escaping () -> Void) {
func performSwitch(to version: String) async {
Log.info("Switching to \(version), unlinking all versions...")
let versions = getVersionsToBeHandled(version)
let group = DispatchGroup()
PhpEnv.shared.availablePhpVersions.forEach { (available) in
group.enter()
DispatchQueue.global(qos: .userInitiated).async {
self.disableDefaultPhpFpmPool(available)
self.stopPhpVersion(available)
group.leave()
await withTaskGroup(of: String.self, body: { group in
for available in PhpEnv.shared.availablePhpVersions {
group.addTask {
await self.disableDefaultPhpFpmPool(available)
await self.stopPhpVersion(available)
return available
}
}
}
group.notify(queue: .global(qos: .userInitiated)) {
Log.info("All versions have been unlinked!")
Log.info("Linking the new version!")
var unlinked: [String] = []
for await version in group {
unlinked.append(version)
}
Log.info("These versions have been unlinked: \(unlinked)")
Log.info("Linking the new version \(version)!")
for formula in versions {
self.startPhpVersion(formula, primary: (version == formula))
Log.info("Will start PHP \(version)... (primary: \(version == formula))")
await self.startPhpVersion(formula, primary: (version == formula))
}
Log.info("Restarting nginx, just to be sure!")
brew("services restart nginx", sudo: true)
await brew("services restart nginx", sudo: true)
Log.info("The new version(s) have been linked!")
completion()
}
})
}
func getVersionsToBeHandled(_ primary: String) -> Set<String> {
let isolated = Valet.shared.sites.filter { site in
site.isolatedPhpVersion != nil
}.map { site in
return site.isolatedPhpVersion!.versionNumber.homebrewVersion
return site.isolatedPhpVersion!.versionNumber.short
}
var versions: Set<String> = [primary]
@ -71,22 +71,22 @@ class InternalSwitcher: PhpSwitcher {
func requiresDisablingOfDefaultPhpFpmPool(_ version: String) -> Bool {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
return FileManager.default.fileExists(atPath: pool)
return FileSystem.fileExists(pool)
}
func disableDefaultPhpFpmPool(_ version: String) {
func disableDefaultPhpFpmPool(_ version: String) async {
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
if FileManager.default.fileExists(atPath: pool) {
if FileSystem.fileExists(pool) {
Log.info("A default `www.conf` file was found in the php-fpm.d directory for PHP \(version).")
let existing = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf")!
let new = URL(string: "file://\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon")!
let existing = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
let new = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf.disabled-by-phpmon"
do {
if FileManager.default.fileExists(atPath: new.path) {
if FileSystem.fileExists(new) {
Log.info("A moved `www.conf.disabled-by-phpmon` file was found for PHP \(version), "
+ "cleaning up so the newer `www.conf` can be moved again.")
try FileManager.default.removeItem(at: new)
try FileSystem.remove(new)
}
try FileManager.default.moveItem(at: existing, to: new)
try FileSystem.move(from: existing, to: new)
Log.info("Success: A default `www.conf` file was disabled for PHP \(version).")
} catch {
Log.err(error)
@ -94,28 +94,28 @@ class InternalSwitcher: PhpSwitcher {
}
}
func stopPhpVersion(_ version: String) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
brew("unlink \(formula)")
brew("services stop \(formula)", sudo: true)
func stopPhpVersion(_ version: String) async {
let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
await brew("unlink \(formula)")
await brew("services stop \(formula)", sudo: true)
Log.info("Unlinked and stopped services for \(formula)")
}
func startPhpVersion(_ version: String, primary: Bool) {
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)"
func startPhpVersion(_ version: String, primary: Bool) async {
let formula = (version == PhpEnv.brewPhpAlias) ? "php" : "php@\(version)"
if primary {
Log.info("\(formula) is the primary formula, linking and starting services...")
brew("link \(formula) --overwrite --force")
await brew("link \(formula) --overwrite --force")
} else {
Log.info("\(formula) is an isolated PHP version, starting services only...")
}
brew("services start \(formula)", sudo: true)
await brew("services start \(formula)", sudo: true)
if Valet.enabled(feature: .isolatedSites) && primary {
let socketVersion = version.replacingOccurrences(of: ".", with: "")
Shell.run("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
await Shell.quiet("ln -sF ~/.config/valet/valet\(socketVersion).sock ~/.config/valet/valet.sock")
Log.info("Symlinked new socket version (valet\(socketVersion).sock → valet.sock).")
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -18,6 +18,6 @@ protocol PhpSwitcherDelegate: AnyObject {
protocol PhpSwitcher {
func performSwitch(to version: String, completion: @escaping () -> Void)
func performSwitch(to version: String) async
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 15/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -0,0 +1,25 @@
//
// Shell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 20/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
var Shell: ShellProtocol {
return ActiveShell.shared
}
class ActiveShell {
static var shared: ShellProtocol = RealShell()
public static func useTestable(_ expectations: [String: BatchFakeShellOutput]) {
Self.shared = TestableShell(expectations: expectations)
}
public static func useSystem() {
Self.shared = RealShell()
}
}

View File

@ -0,0 +1,192 @@
//
// RealShell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Process: @unchecked Sendable {}
extension Timer: @unchecked Sendable {}
class RealShell: ShellProtocol {
/**
The launch path of the terminal in question that is used.
On macOS, we use /bin/sh since it's pretty fast.
*/
private(set) var launchPath: String = "/bin/sh"
/**
For some commands, we need to know what's in the user's PATH.
The entire PATH is retrieved here, so we can set the PATH in our own terminal as necessary.
*/
private(set) var PATH: String = { return RealShell.getPath() }()
/**
Exports are additional environment variables set by the user via the custom configuration.
These are populated when the configuration file is being loaded.
*/
var exports: String = ""
/** Retrieves the user's PATH by opening an interactive shell and echoing $PATH. */
private static func getPath() -> String {
let task = Process()
task.launchPath = "/bin/zsh"
// We need an interactive shell so the user's PATH is loaded in correctly
task.arguments = ["--login", "-ilc", "echo $PATH"]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
return String(
data: pipe.fileHandleForReading.readDataToEndOfFile(),
encoding: String.Encoding.utf8
) ?? ""
}
/**
Create a process that will run the required shell with the appropriate arguments.
This process still needs to be started, or one can attach output handlers.
*/
private func getShellProcess(for command: String) -> Process {
var completeCommand = ""
// Basic export (PATH)
completeCommand += "export PATH=\(Paths.binPath):$PATH && "
// Put additional exports (as defined by the user) in between
if !self.exports.isEmpty {
completeCommand += "\(self.exports) && "
}
completeCommand += command
let task = Process()
task.launchPath = self.launchPath
task.arguments = ["--noprofile", "-norc", "--login", "-c", completeCommand]
return task
}
// MARK: - Public API
/**
Set custom environment variables.
These will be exported when a command is executed.
*/
public func setCustomEnvironmentVariables(_ variables: [String: String]) {
self.exports = variables.map { (key, value) in
return "export \(key)=\(value)"
}.joined(separator: "&&")
}
// MARK: - Shellable Protocol
func pipe(_ command: String) async -> ShellOutput {
let task = getShellProcess(for: command)
let outputPipe = Pipe()
let errorPipe = Pipe()
// Seriously slow down how long it takes for the shell to return output
// (in order to debug or identify async issues)
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
Log.info("[SLOW SHELL] \(command)")
await delay(seconds: 3.0)
}
task.standardOutput = outputPipe
task.standardError = errorPipe
task.launch()
task.waitUntilExit()
let stdOut = String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!
let stdErr = String(
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8
)!
if Log.shared.verbosity == .cli {
var args = task.arguments ?? []
let last = "\"" + (args.popLast() ?? "") + "\""
var log = """
<~~~~~~~~~~~~~~~~~~~~~~~
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
[OUT]:
\(stdOut)
"""
if !stdErr.isEmpty {
log.append("""
[ERR]:
\(stdErr)
""")
}
log.append("""
~~~~~~~~~~~~~~~~~~~~~~~~>
""")
Log.info(log)
}
return .out(stdOut, stdErr)
}
func quiet(_ command: String) async {
_ = await self.pipe(command)
}
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval = 5.0
) async throws -> (Process, ShellOutput) {
let process = getShellProcess(for: command)
let output = ShellOutput.empty()
process.listen { incoming in
output.out += incoming; didReceiveOutput(incoming, .stdOut)
} didReceiveStandardErrorData: { incoming in
output.err += incoming; didReceiveOutput(incoming, .stdErr)
}
return try await withCheckedThrowingContinuation({ continuation in
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in
// Only terminate if the process is still running
if process.isRunning {
process.terminationHandler = nil
process.terminate()
return continuation.resume(throwing: ShellError.timedOut)
}
}
process.terminationHandler = { [timer, output] process in
timer.invalidate()
process.haltListening()
if !output.err.isEmpty {
return continuation.resume(returning: (process, .err(output.err)))
}
return continuation.resume(returning: (process, .out(output.out)))
}
process.launch()
process.waitUntilExit()
})
}
}

View File

@ -0,0 +1,83 @@
//
// ShellProtocol.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
protocol ShellProtocol {
/**
The PATH for the current shell.
*/
var PATH: String { get }
/**
Run a command asynchronously.
Returns the most relevant output (prefers error output if it exists).
Common usage:
```
let output = await Shell.pipe("php -v")
```
*/
func pipe(_ command: String) async -> ShellOutput
/**
Run a command asynchronously, without returning the output of the command.
Returns the most relevant output (prefers error output if it exists).
*/
func quiet(_ command: String) async
/**
Runs a command asynchronously, and fires closure with `stdout` or `stderr` data as it comes in.
You can specify how long this task should run.
The process will always be terminated after the specified time interval.
(Whether it is complete or not.)
Unlike `sync`, `pipe` and `quiet`, you can capture both `stdout` and `stderr` with this mechanism.
The end result is still the most relevant output (where error output is preferred if it exists).
*/
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput)
}
enum ShellStream: Codable {
case stdOut, stdErr, stdIn
}
class ShellOutput {
var out: String
var err: String
var hasError: Bool {
return err.lengthOfBytes(using: .utf8) > 0
}
init(out: String, err: String) {
self.out = out
self.err = err
}
static func empty() -> ShellOutput {
return ShellOutput(out: "", err: "")
}
static func out(_ out: String?, _ err: String? = nil) -> ShellOutput {
return ShellOutput(out: out ?? "", err: err ?? "")
}
static func err(_ err: String?) -> ShellOutput {
return ShellOutput(out: "", err: err ?? "")
}
}
enum ShellError: Error {
case timedOut
}

View File

@ -0,0 +1,27 @@
//
// TestableCommand.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 12/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class TestableCommand: CommandProtocol {
init(commands: [String: String]) {
self.commands = commands
}
var commands: [String: String]
func execute(path: String, arguments: [String]) -> String {
self.execute(path: path, arguments: arguments, trimNewlines: false)
}
public func execute(path: String, arguments: [String], trimNewlines: Bool) -> String {
let concatenatedCommand = "\(path) \(arguments.joined(separator: " "))"
assert(commands.keys.contains(concatenatedCommand), "Command `\(concatenatedCommand)` not found")
return self.commands[concatenatedCommand]!
}
}

View File

@ -0,0 +1,67 @@
//
// TestableConfiguration.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 16/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
public struct TestableConfiguration: Codable {
var architecture: String
var filesystem: [String: FakeFile]
var shellOutput: [String: BatchFakeShellOutput]
var commandOutput: [String: String]
func apply() {
Log.separator()
Log.info("USING TESTABLE CONFIGURATION...")
Homebrew.fake = true
Log.separator()
Log.info("Applying fake shell...")
ActiveShell.useTestable(shellOutput)
Log.info("Applying fake filesystem...")
ActiveFileSystem.useTestable(filesystem)
Log.info("Applying fake commands...")
ActiveCommand.useTestable(commandOutput)
Log.info("Applying fake scanner...")
ValetScanner.useFake()
Log.info("Applying fake services manager...")
ServicesManager.useFake()
Log.info("Applying fake Valet domain interactor...")
ValetInteractor.useFake()
}
func toJson(pretty: Bool = false) -> String {
let data = try! JSONEncoder().encode(self)
if pretty {
return data.prettyPrintedJSONString! as String
}
return String(data: data, encoding: .utf8)!
}
static func loadFrom(path: String) -> TestableConfiguration {
let url = URL(fileURLWithPath: path.replacingTildeWithHomeDirectory)
if !FileManager.default.fileExists(atPath: url.path) {
/*
You will need to run the `TestableConfigurationTest` test,
which will generate two configuration files you can use.
*/
fatalError("Error: the expected configuration file at \(url.path) is missing!")
}
/*
If the decoder below fails to decode the configuration file,
the configuration may have been updated.
In that case, you will need to run the test (see above) again.
*/
return try! JSONDecoder().decode(
TestableConfiguration.self,
from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
)
}
}

View File

@ -0,0 +1,288 @@
//
// TestableFileSystem.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/10/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
class TestableFileSystem: FileSystemProtocol {
/**
Initialize a fake filesystem with a bunch of files.
You do not need to specify directories (unless symlinks), those will be created automatically.
*/
init(files: [String: FakeFile]) {
self.files = files
// Ensure that each of the ~ characters are replaced with the home directory path
for key in self.files.keys where key.contains("~") {
self.files.renameKey(
fromKey: key,
toKey: key.replacingOccurrences(of: "~", with: self.homeDirectory)
)
}
// Ensure that intermediate directories are created
for file in self.files {
self.createIntermediateDirectories(file.key)
}
}
/**
Internal file handling of the fake filesystem.
You can easily dump what's in here by using:
```
let fs = FileSystem as! TestableFileSystem
fs.printContents()
```
*/
private(set) var files: [String: FakeFile]
/**
The home directory for the fake filesystem.
*/
private(set) var homeDirectory = "/Users/fake"
// MARK: - Basics
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
let path = path.replacingTildeWithHomeDirectory
if files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
self.createIntermediateDirectories(path)
self.files[path] = .fake(.directory)
}
func writeAtomicallyToFile(_ path: String, content: String) throws {
let path = path.replacingTildeWithHomeDirectory
if files[path] != nil {
throw TestableFileSystemError.alreadyExists
}
self.files[path] = .fake(.text, content)
}
func getStringFromFile(_ path: String) throws -> String {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
return file.content ?? ""
}
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
let path = path.replacingTildeWithHomeDirectory
var seek = path
if !seek.hasSuffix("/") {
seek = "\(seek)/"
}
return self.files.keys
.filter { $0.hasPrefix(seek) }
.map { $0.replacingOccurrences(of: seek, with: "") }
.filter { !$0.contains("/") }
}
func getDestinationOfSymlink(_ path: String) throws -> String {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
if file.type != .symlink {
throw TestableFileSystemError.notSymlink
}
guard let pathToSymlink = file.content else {
throw TestableFileSystemError.invalidSymlink
}
if !files.keys.contains(pathToSymlink) {
throw TestableFileSystemError.invalidSymlink
}
return pathToSymlink
}
// MARK: - Move & Delete Files
func move(from path: String, to newPath: String) throws {
let path = path.replacingTildeWithHomeDirectory
let newPath = newPath.replacingTildeWithHomeDirectory
self.files.keys.forEach { key in
if key.hasPrefix(path) {
self.files.renameKey(
fromKey: key,
toKey: key.replacingOccurrences(of: path, with: newPath)
)
}
}
self.files.renameKey(fromKey: path, toKey: newPath)
}
func remove(_ path: String) throws {
// Remove recursively
self.files.keys.forEach { key in
if key.hasPrefix(path) {
self.files.removeValue(forKey: key)
}
}
self.files.removeValue(forKey: path)
}
// MARK: Attributes
func makeExecutable(_ path: String) throws {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
throw TestableFileSystemError.fileMissing
}
file.type = .binary
}
// MARK: - Checks
func isExecutableFile(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path.replacingTildeWithHomeDirectory] else {
return false
}
return file.type == .binary
}
func isWriteableFile(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path.replacingTildeWithHomeDirectory] else {
return false
}
return !file.readOnly
}
func anyExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
return files.keys.contains(path)
}
func fileExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return [.binary, .symlink, .text].contains(file.type)
}
func directoryExists(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return [.directory].contains(file.type)
}
func isSymlink(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return file.type == .symlink
}
func isDirectory(_ path: String) -> Bool {
let path = path.replacingTildeWithHomeDirectory
guard let file = files[path] else {
return false
}
return file.type == .directory
}
public func printContents() {
for key in self.files.keys.sorted() {
print("\(key) -> \(self.files[key]!.type)")
}
}
private func createIntermediateDirectories(_ path: String) {
let path = path.replacingTildeWithHomeDirectory
let items = path.components(separatedBy: "/")
var preceding = ""
for item in items {
let key = preceding == "/"
? "/\(item)"
: "\(preceding)/\(item)"
if !self.files.keys.contains(key) {
self.files[key] = .fake(.directory)
}
preceding = key
}
}
}
enum FakeFileType: Codable {
case binary, text, directory, symlink
}
class FakeFile: Codable {
var type: FakeFileType
var content: String?
var readOnly: Bool = false
init(type: FakeFileType, content: String?, readOnly: Bool = false) {
self.type = type
self.content = content
self.readOnly = readOnly
}
public static func fake(
_ type: FakeFileType,
_ content: String? = nil,
readOnly: Bool = false
) -> FakeFile {
return FakeFile(
type: type,
content: content,
readOnly: readOnly
)
}
}
enum TestableFileSystemError: Error {
case fileMissing
case alreadyExists
case notSymlink
case invalidSymlink
}

View File

@ -0,0 +1,123 @@
//
// TestableShell.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 21/09/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
public class TestableShell: ShellProtocol {
var PATH: String {
return "/usr/local/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin"
}
init(expectations: [String: BatchFakeShellOutput]) {
self.expectations = expectations
}
var expectations: [String: BatchFakeShellOutput] = [:]
func quiet(_ command: String) async {
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
}
func pipe(_ command: String) async -> ShellOutput {
let (_, output) = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
return output
}
func attach(
_ command: String,
didReceiveOutput: @escaping (String, ShellStream) -> Void,
withTimeout timeout: TimeInterval
) async throws -> (Process, ShellOutput) {
// Seriously slow down the shell's return rate in order to debug or identify async issues
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
Log.info("[SLOW SHELL] \(command)")
await delay(seconds: 3.0)
}
// This assertion will only fire during test builds
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
guard let expectation = expectations[command] else {
return (Process(), .err("No Expected Output"))
}
let output = await expectation.output(didReceiveOutput: { output, type in
didReceiveOutput(output, type)
}, ignoreDelay: isRunningTests)
return (Process(), output)
}
}
struct FakeShellOutput: Codable {
let delay: TimeInterval
let output: String
let stream: ShellStream
static func instant(_ output: String, _ stream: ShellStream = .stdOut) -> FakeShellOutput {
return FakeShellOutput(delay: 0, output: output, stream: stream)
}
static func delayed(_ delay: TimeInterval, _ output: String, _ stream: ShellStream = .stdOut) -> FakeShellOutput {
return FakeShellOutput(delay: delay, output: output, stream: stream)
}
}
struct BatchFakeShellOutput: Codable {
var items: [FakeShellOutput]
static func with(_ items: [FakeShellOutput]) -> BatchFakeShellOutput {
return BatchFakeShellOutput(items: items)
}
static func instant(_ output: String, _ stream: ShellStream = .stdOut) -> BatchFakeShellOutput {
return BatchFakeShellOutput(items: [.instant(output, stream)])
}
static func delayed(
_ delay: TimeInterval,
_ output: String,
_ stream: ShellStream = .stdOut
) -> BatchFakeShellOutput {
return BatchFakeShellOutput(items: [.delayed(delay, output, stream)])
}
/**
Outputs the fake shell output as expected.
*/
public func output(
didReceiveOutput: @escaping (String, ShellStream) -> Void,
ignoreDelay: Bool = false
) async -> ShellOutput {
let output = ShellOutput.empty()
for item in items {
if !ignoreDelay {
await delay(seconds: item.delay)
}
if item.stream == .stdErr {
output.err += item.output
} else if item.stream == .stdOut {
output.out += item.output
}
}
return output
}
/**
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
*/
public func outputInstantaneously(
didReceiveOutput: @escaping (String, ShellStream) -> Void = { _, _ in }
) async -> ShellOutput {
return await self.output(didReceiveOutput: didReceiveOutput, ignoreDelay: true)
}
}

View File

@ -16,7 +16,7 @@
<p><b>Do you enjoy using the app?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
<p><b>Get the latest on Twitter</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> to learn about the latest and greatest updates of this app.</p>
<p><b>Get the latest on Mastodon.</b> Give me a <a href="https://phpc.social/@nicoverbruggen">follow on Mastodon</a> to learn about what's brewing and when new updates drop.</p>
<br>
</body>
</html>

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa

View File

@ -2,7 +2,7 @@
// StateManager.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -31,10 +31,18 @@ class App {
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
}
/** Just the bundle name. */
static var identifier: String {
Bundle.main.infoDictionary?["CFBundleIdentifier"] as! String
}
/** The system architecture. Paths differ based on this value. */
static var architecture: String {
if fakeArchitecture != nil { return fakeArchitecture! }
var systeminfo = utsname()
uname(&systeminfo)
let machine = withUnsafeBytes(of: &systeminfo.machine) {bufPtr->String in
let machine = withUnsafeBytes(of: &systeminfo.machine) { bufPtr -> String in
let data = Data(bufPtr)
if let lastIndex = data.lastIndex(where: {$0 != 0}) {
return String(data: data[0...lastIndex], encoding: .isoLatin1)!
@ -45,8 +53,18 @@ class App {
return machine
}
/**
A fake architecture.
When set, the real machine's system architecture is not used,
but this fixed value is used instead.
*/
static var fakeArchitecture: String?
// MARK: Variables
/** Technical information about the current environment. */
var environment = EnvironmentManager()
/** The list of preferences that are currently active. */
var preferences: [PreferenceName: Bool]!
@ -65,9 +83,6 @@ class App {
/** List of detected (installed) applications that PHP Monitor can work with. */
var detectedApplications: [Application] = []
/** The services manager, responsible for figuring out what services are active/inactive. */
var services = ServicesManager.shared
/** The warning manager, responsible for keeping track of warnings. */
var warnings = WarningManager.shared

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 20/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -20,8 +20,7 @@ extension AppDelegate {
Please note that PHP Monitor needs to be running in the background for this to work.
*/
func application(_ application: NSApplication, open urls: [URL]) {
@MainActor func application(_ application: NSApplication, open urls: [URL]) {
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
Log.info("Acting on commands via phpmon:// has been disabled.")
return

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
@ -33,15 +33,17 @@ extension AppDelegate {
}
@IBAction func reloadDomainListPressed(_ sender: Any) {
let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
Task { // Reload domains
let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
if vc != nil {
// If the view exists, directly reload the list of sites
vc!.reloadDomains()
} else {
// If the view does not exist, reload the cached data that was populated when the app initially launched.
Valet.shared.reloadSites()
if vc != nil {
// If the view exists, directly reload the list of sites.
await vc!.reloadDomains()
} else {
// If the view does not exist, reload the cached data that was populated when the app launched.
await Valet.shared.reloadSites()
}
}
}

View File

@ -3,7 +3,7 @@
// PHP Monitor
//
// Created by Nico Verbruggen on 06/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation

View File

@ -2,7 +2,7 @@
// AppDelegate.swift
// PHP Monitor
//
// Copyright © 2022 Nico Verbruggen. All rights reserved.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Cocoa
@ -13,13 +13,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: - Variables
/**
The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application.
*/
let sharedShell: Shell
/**
The App singleton contains information about the state of
the application and global variables.
@ -63,19 +56,34 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
When the application initializes, create all singletons.
*/
override init() {
logger.verbosity = .info
#if DEBUG
// logger.verbosity = .performance
logger.verbosity = .performance
if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) {
Self.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: ""))
}
#endif
if CommandLine.arguments.contains("--v") {
logger.verbosity = .performance
Log.info("Extra verbose mode has been activated.")
}
if CommandLine.arguments.contains("--cli") {
logger.verbosity = .cli
Log.info("Extra CLI mode has been activated via --cli flag.")
}
if FileSystem.fileExists("~/.config/phpmon/verbose") {
logger.verbosity = .cli
Log.info("Extra CLI mode is on (`~/.config/phpmon/verbose` exists).")
}
Log.separator(as: .info)
Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)")
Log.separator(as: .info)
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
self.paths = Paths.shared
@ -87,6 +95,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.phpEnvironment = PhpEnv.shared
}
static func initializeTestingProfile(_ path: String) {
Log.info("The configuration with path `\(path)` is being requested...")
TestableConfiguration.loadFrom(path: path).apply()
}
// MARK: - Lifecycle
/**
@ -97,8 +110,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Make sure notifications will work
setupNotifications()
// Make sure the menu performs its initial checks
Task { await menu.startup() }
Task { // Make sure the menu performs its initial checks
await menu.startup()
}
}
}

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