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

Compare commits

...

183 Commits

Author SHA1 Message Date
e0c087dbcb 🚀 Version 5.3.2 2022-06-14 18:23:09 +02:00
507d4785aa 🚀 Version 5.3.1 2022-06-08 18:36:24 +02:00
c645bb7610 🚀 Version 5.3
Merge branch 'dev/5.3'
2022-05-13 16:33:48 +02:00
e6574966da 📝 Clarify network requests 2022-05-13 00:38:42 +02:00
1e9cfff05e 📝 Updated README 2022-05-13 00:34:57 +02:00
bd34c2b255 👌 Improved nginx file parsing 2022-05-13 00:24:54 +02:00
c040ac3200 🔧 Prepare for release build 2022-05-10 19:20:47 +02:00
6c6888c9cb 👌 Bump version number for new beta build 2022-05-10 19:02:37 +02:00
78cb6922b3 🐛 Fix issue with version parser 2022-05-10 19:01:37 +02:00
c16377c688 👌 Improved updater 2022-05-10 18:52:48 +02:00
540ea5c310 👌 Changes to updater 2022-05-10 18:34:34 +02:00
4ba2b25f18 👌 App version parsing 2022-05-10 18:09:22 +02:00
81b75dcaa8 👌 Async unlink and unproxy to prevent main thread hang 2022-05-10 10:44:24 +02:00
884784d024 🐛 Handle trailing semicolon (#170) 2022-05-10 10:26:48 +02:00
e81ff2870d Add test and prepare for new prerelease 2022-05-10 01:00:18 +02:00
7c631099b2 👌 Fix regular expression 2022-05-10 00:43:17 +02:00
f7a98b88a7 👌 Improve proxy subject validation 2022-05-10 00:38:13 +02:00
3fc21fff2a ♻️ Cleanup 2022-05-10 00:18:45 +02:00
0306c2b726 ♻️ Clarify parameter name 2022-05-10 00:16:47 +02:00
9d822df54e Check for updates 2022-05-10 00:14:48 +02:00
f413b84a45 🏗 WIP: Check for updates 2022-05-09 23:41:52 +02:00
b82811e6bf 🐛 Fix issue with tertiary action 2022-05-09 23:01:36 +02:00
af922664ab 🏗 WIP: Check for updates 2022-05-09 17:28:35 +02:00
8b73e69495 🐛 Fix issue with listing extensions 2022-05-09 15:27:55 +02:00
29a9e14741 🐛 Fix crash issue with .DS_Store 2022-05-09 15:27:43 +02:00
997fb27596 👌 Update copyright, verbose logging tweak 2022-05-07 18:43:36 +02:00
c171df0a93 Add additional verbosity option (#169) 2022-05-07 13:32:45 +02:00
1c15a4e07f Add (un)secure option for proxies 2022-05-06 18:27:03 +02:00
5067c7b87f 🏗 WIP: Add secure/unsecure option for proxy 2022-05-05 21:29:03 +02:00
2987464da8 📝 Added information about linter 2022-05-03 18:20:11 +02:00
4d04275c57 Added linting 2022-05-03 18:16:26 +02:00
790f63e8c9 🔧 Disable Xdebug item for 5.3 2022-05-02 18:26:02 +02:00
86b49812c3 ♻️ Cleanup menu item generation (#168) 2022-05-02 18:24:44 +02:00
ef9e0fd916 Begin work on Xdebug mode switcher (#168) 2022-05-01 22:07:18 +02:00
af8807f799 🔀 Merge bugfixes from 5.2 into 5.3 2022-04-23 12:25:18 +02:00
4eea13f059 🚀 Version 5.2.2
Merge branch 'dev/5.2'
2022-04-22 19:50:57 +02:00
ba93ed93e4 Allow opening of proxies in browser 2022-04-21 19:18:05 +02:00
932a0fe176 Add 'Remove Proxy' to right-click menu 2022-04-21 19:08:40 +02:00
3cff2d6469 🐛 Fix issue w/ flag evaluation order (#165) 2022-04-20 22:31:20 +02:00
df506e4128 🐛 Fix various minor issues (discovered via #162)
- Renaming the configuration files from `www.conf` to the backup
  (`disabled-by-phpmon`) will now succeed if the `disabled-by-phpmon`
  file already exists. This would fail if the `disabled-by-phpmon` file
  already existed in previous builds.

- The PHP-FPM alert when there's an issue with a missing socket
  configuration file has been tweaked and now contains a workaround if
  you want to run a newer version of PHP (e.g. PHP 8.2) that is not
  officially supported by Valet yet.

- When attempting to list the PHP version numbers, the `parse()` method
  is now used, as opposed to `PhpVersionNumber.make()`, which couldn't
  correctly handle pre-release versions of PHP.

- Updated tests to reflect these changes to `PhpVersionNumber`.
2022-04-20 12:19:02 +02:00
eb80214785 ✏️ Updated copy 2022-04-19 20:39:51 +02:00
80a4e361a4 🔧 Prepare for new DEV build 2022-04-19 20:33:19 +02:00
2af88b2bee ✏️ Update copy about non-standard TLDs 2022-04-19 20:26:44 +02:00
5048ccab8c 👌 Update various TODO items 2022-04-18 12:00:54 +02:00
66d13c92d5 Run the valet proxy command (#105) 2022-04-18 11:59:14 +02:00
836b076da9 👌 Cleanup, ensure dynamic form works correctly 2022-04-17 14:33:59 +02:00
1a75838a3b Set up proxy view strings and outlets 2022-04-17 14:02:44 +02:00
a18b7962a7 ♻️ Fix link 2022-04-16 23:21:29 +02:00
84548634ec Add proxy view 2022-04-16 23:19:13 +02:00
419ebe61f7 Add selection view 2022-04-14 14:56:51 +02:00
c45817b127 Added new UI for proxies to storyboard 2022-04-13 19:14:08 +02:00
2c0c0c5a11 Correctly detect secured proxies 2022-04-12 20:43:57 +02:00
1b8d6311ba ♻️ Refactor displaying domains 2022-04-12 17:36:18 +02:00
f0f7a3f7d6 👌 Scan proxies (#105) 2022-04-11 22:56:40 +02:00
8304d774c3 ♻️ Refactoring of files and tests 2022-04-02 15:48:21 +02:00
faeea4e866 Ensure normal nginx file does not have proxy 2022-03-31 18:28:47 +02:00
6470daf7d3 Added test to parse the proxy address 2022-03-31 18:27:26 +02:00
94139a3669 🚚 Moved test files into separate directories 2022-03-31 18:09:50 +02:00
f3b1172d0e 🚀 Version 5.2.1
This is a bugfix release that fixes the isolation command (#158).
2022-03-31 13:51:46 +02:00
8057019898 🐛 Fix isolation command (#158) 2022-03-31 13:35:23 +02:00
9b59fc5dae 👌 Cleanup proxies 2022-03-31 13:34:56 +02:00
75f4377de8 Added tooltips next to PHP version 2022-03-31 13:29:04 +02:00
d3657716c4 ♻️ Added ValetProxy struct 2022-03-30 21:26:53 +02:00
a13990b96f ♻️ Rename SiteList to DomainList 2022-03-30 19:19:36 +02:00
4c7aa7fead Add UI for displaying proxies (#105) 2022-03-29 21:40:10 +02:00
32278533bf 🚀 Version 5.2
Merge branch 'dev/5.x'
2022-03-29 17:32:26 +02:00
39908f7fc5 🔧 Bump build 2022-03-29 17:32:09 +02:00
347d79c88d 👌 Cleanup 2022-03-29 17:25:47 +02:00
9bc117e9f5 🔥 Remove icon 2022-03-29 17:18:44 +02:00
ba5fbed9be 📝 Add logo 2022-03-29 17:18:26 +02:00
211556d5ce 📝 Include system administrator requirement 2022-03-29 14:16:28 +02:00
066d7bc217 🐛 Restoring Homebrew permissions uses admin group now 2022-03-29 14:14:39 +02:00
f072ceae37 📝 Updated FAQ 2022-03-29 09:55:38 +02:00
273dac1ca7 📝 Updated README with PHP upgrade instructions 2022-03-29 09:51:56 +02:00
f3b170ba14 🔧 Prepare for release 2022-03-29 09:23:57 +02:00
78d8030ed6 Fix tests 2022-03-25 23:55:41 +01:00
a300d2f4cf 👌 Show isolation is unavailable 2022-03-25 23:54:32 +01:00
96a658073e 📝 Update README and SECURITY 2022-03-25 17:26:58 +01:00
8395ba407d 👌 Use new, cleaner layout for screenshot 2022-03-25 17:11:40 +01:00
f80e3fed2b 👌 Disable border on table 2022-03-25 16:40:54 +01:00
b48edf7409 📝 New screenshot in README 2022-03-23 19:50:58 +01:00
e0a0eb089d 🔧 Prepare for a new dev build 2022-03-23 18:52:54 +01:00
60b126333d 👌 Extra startup check for invalid config.json
- Up to 50 sites are now preloaded (up from 30)
- No longer crash when invalid config.json is found (only at launch)
- Added `evaluateFeatureSupport` to Valet.swift
- Load configuration during launch checks instead
2022-03-23 18:46:35 +01:00
649a3f4fb5 👌 Handle correct version number 2022-03-23 18:15:17 +01:00
9a326928f3 🐛 Ensure unknown driver is at bottom 2022-03-21 18:45:41 +01:00
52f87ca18a 👌 Add tooltips to First Aid menu items 2022-03-21 18:33:25 +01:00
ad2d2cb57f Allow sorting of site list 2022-03-21 18:33:12 +01:00
22c0021ada 🐛 Workaround broken JSON file (AddSiteVC.swift:66) 2022-03-20 15:56:07 +01:00
2cfc5731fb 👌 Re-enable debugger, site loading 2022-03-20 15:07:34 +01:00
1d74e1536c Add default site to site list (#125) 2022-03-20 14:38:58 +01:00
e6b2ddf2ad 👌 Fix "WordPress" name in fake site window 2022-03-19 15:54:17 +01:00
62fda6224e 👌 Updated fake sites 2022-03-19 15:21:50 +01:00
083f8ebec8 Add marketing mode 2022-03-19 15:15:03 +01:00
aec8fb1168 🔧 Prepare for a new dev build 2022-03-18 18:38:11 +01:00
d5d9b38ed6 ♻️ Reworked "Fix My Valet" to use built-in switcher 2022-03-18 18:36:49 +01:00
9a6975e3d9 ♻️ Include HotKey source code 2022-03-18 18:21:55 +01:00
d1c40f2eb5 🐛 Fix issue with generator 2022-03-18 15:54:53 +01:00
ad58661449 🔥 Cleanup 2022-03-17 23:32:40 +01:00
1d6cfd419e Fix tests 2022-03-17 23:32:13 +01:00
15182ea15a 👌 Cleanup writing helper files 2022-03-17 23:28:03 +01:00
c43e00c88d 👌 Add source_php{version} helpers 2022-03-17 21:21:05 +01:00
25c7004368 🔥 Remove impossible functionality 2022-03-17 20:20:23 +01:00
02ba57cd64 🏗 TODO: Automatic isolated PHP handling 2022-03-17 19:54:13 +01:00
c2dc6302c0 ️ Fix performance issue with async 2022-03-17 19:18:48 +01:00
af9f30a123 ️ Fix performance bottleneck related to view drawing 2022-03-17 18:38:26 +01:00
28c5754800 🐛 Disable www.conf file when switching versions (#152) 2022-03-17 18:03:00 +01:00
48c1d48573 👌 Add support for Typo3 (#153) 2022-03-17 17:49:10 +01:00
582bf0e12c 🐛 Fix storyboard 2022-03-17 17:48:50 +01:00
46b30bbff4 🐛 Ensure isolation is available 2022-03-16 23:36:53 +01:00
372011ca08 Add site isolation option to context menu 2022-03-16 23:30:26 +01:00
7255792910 ♻️ WIP: Various changes 2022-03-16 22:17:17 +01:00
0c96b11b05 ♻️ WIP: Modified layout 2022-03-16 19:40:01 +01:00
ea4da12d3b ♻️ WIP: Cell rendering in site list 2022-03-16 19:15:57 +01:00
8419ebad10 👌 Adjust internal switcher 2022-03-16 18:04:45 +01:00
09a5cf836a 📝 Added TODO comments 2022-03-15 22:40:40 +01:00
1a1a53b472 ♻️ Preliminary refactor for Valet 3.0 (#148) 2022-03-15 22:39:46 +01:00
a8bad8447a 👌 isolatedVersion as a lazy property 2022-03-15 18:27:38 +01:00
ca8f5a8fbe Speed up nginx parsing 2022-03-15 18:16:28 +01:00
a0e7aec228 Parse nginx files + tests 2022-03-14 23:35:08 +01:00
26badc759e 🏗 Conditional PHP 5.6 support 2022-03-14 21:31:30 +01:00
e21c2168ea 🏗 Add isolation icon 2022-03-14 19:39:04 +01:00
589ab3664e 🏗 Add feature flag based on Valet version 2022-03-14 19:19:14 +01:00
48b4f9b160 🏗 Added initial TODO items for #148 2022-03-14 19:13:38 +01:00
139e416c3b 🚀 Version 5.1.1
Merge branch 'dev/5.x'
2022-03-09 17:52:07 +01:00
ba4ed3b365 🔧 Prepare for maintenance release 2022-03-09 17:51:19 +01:00
06a8022265 📝 Annotate environment checks (#146) 2022-03-09 16:41:47 +01:00
3b297e07dc 🐛 Adjust the order of startup checks (#146) 2022-03-09 16:19:41 +01:00
68fa8e523e 👌 Fix string that made it into 5.1 2022-03-03 14:49:33 +01:00
768bf06a9d 🚀 Version 5.1
Merge branch 'dev/5.x'
2022-03-02 19:53:52 +01:00
6a8d66758a 🔧 Build 715 for the final 5.1 release 2022-03-02 19:52:24 +01:00
078e6e6f23 📝 Update README with Raycast extension 2022-03-01 09:32:01 +01:00
3f80bfb641 👌 Detect binary paths (#144)
This change introduces binary detection to the app. PHP Monitor does not
rely on the user's PATH because the output of a user's terminal can
cause issues, so we will scan the two common locations for the Composer
binary file. The text in the alert has been modified to accommodate the
change (you could still not have Composer installed).
2022-02-26 18:24:02 +01:00
a34389c3a9 🔧 New dev build (RC) 2022-02-23 14:07:14 +01:00
692d3c143f ♻️ Refactor Composer window logic 2022-02-23 13:39:49 +01:00
bc86a45925 Run tests in isolation 2022-02-23 13:03:04 +01:00
2a412b794a Fix tests 2022-02-22 21:16:50 +01:00
bf673263d8 👌 Extract method 2022-02-22 21:16:17 +01:00
ce498d3019 ♻️ Cleanup 2022-02-22 20:49:13 +01:00
e398f089af ♻️ Refactor Valet structure, add #143 2022-02-22 20:39:35 +01:00
e8ba24e48b 👌 Improved alerts 2022-02-20 16:39:06 +01:00
e0f40be188 🐛 Fix issue with "Fix Homebrew Permissions" 2022-02-20 16:33:56 +01:00
42848764cf 👌 New icon for DEV builds 2022-02-20 16:08:04 +01:00
46ac0c339c 👌 Warn about custom TLD (#126) 2022-02-20 15:46:51 +01:00
a0fe68f3ab 👌 Constants as struct 2022-02-20 15:29:24 +01:00
c952c3d031 👌 Link to FAQ 2022-02-20 15:29:15 +01:00
c05cdeda72 👌 Offer to switch back to prev version (#141) 2022-02-20 15:12:30 +01:00
ae7b285eb0 🐛 Fix delay due to use of async 2022-02-20 14:26:56 +01:00
6b3c562af2 🐛 Fixes full PHP version (#142) 2022-02-20 13:48:37 +01:00
e3ae878cae 👌 Various alerts updated 2022-02-18 22:01:05 +01:00
dd43c94e6e 👌 Handle errors 2022-02-18 20:38:40 +01:00
0e8fe1fcfb 👌 Updated all environment alerts 2022-02-18 20:13:43 +01:00
293b7f564e 🏗 Rearrange button order 2022-02-17 19:45:56 +01:00
634ffb4c57 🏗 Preparing for additional refactoring 2022-02-17 19:23:23 +01:00
9468a2e9f8 🚩 Break compilation
Now missing labels will come up as Compiler Errors. This, along with the
deprecations in the previous alert should allow me to replace all the
alerts everywhere.
2022-02-17 19:21:03 +01:00
921ecd99d6 👌 Improve BetterAlert performance notices 2022-02-17 19:19:02 +01:00
d06f92051d 👌 Improved alert for sponsor encouragement 2022-02-17 19:07:20 +01:00
97d68f89f1 👌 Animate custom modal, fix constraints 2022-02-17 18:43:48 +01:00
115863f1ee 👌 Improve initial alert 2022-02-17 09:59:50 +01:00
bc27a4d25a 👌 Cleanup 2022-02-17 00:14:40 +01:00
5a0b2f319b 👌 Use BetterAlert API 2022-02-16 23:51:06 +01:00
50f003a2bd New notice alert API 2022-02-16 22:42:43 +01:00
bc0b50f5bf 👌 Fixed UI and added new notice view 2022-02-16 21:11:33 +01:00
dd20b25900 📝 Add issue templates 2022-02-14 13:56:01 +01:00
d0469467ac 📝 Add issue templates (#138) 2022-02-14 13:55:16 +01:00
61427ec505 👌 Cleanup App.busy -> PhpEnv.shared.isBusy 2022-02-12 15:08:00 +01:00
6cd1d78572 👌 Tweak logger verbosity 2022-02-12 14:58:49 +01:00
0ad5049785 Add separator to Log 2022-02-12 14:52:10 +01:00
b5c1960260 👌 Async / await support for loading services 2022-02-12 14:47:29 +01:00
e6fbe7c4a4 👀 First attempt at using async code in Swift 2022-02-12 13:17:17 +01:00
537536d443 👌 Improve initial logging during startup 2022-02-11 23:51:58 +01:00
f702d14749 👌 Move check list to bottom 2022-02-11 23:39:43 +01:00
74bb544f3c ♻️ Refactor (part 2) 2022-02-11 23:37:44 +01:00
dae47e3779 ♻️ Refactor startup procedure 2022-02-11 23:24:38 +01:00
dc91d0e00c 👌 Path based on architecture (#134) 2022-02-11 18:58:07 +01:00
6187eb3e4e 👌 Communicate why Fix My Valet is disabled (#135)
A quick 5-minute fix.

As raised in #135, it is not super obvious why Fix My Valet might be
disabled. From now on, Fix My Valet is now always enabled, but it might
throw up an alert if the required formula is missing.
2022-02-11 14:19:52 +01:00
0fa6d337f2 👌 Wrote down some notes during Deep Dive 2022-02-09 22:53:37 +01:00
a10e1cad11 🔥 Remove RELEASE.md (and move it to DEVELOPER.md) 2022-02-09 21:49:30 +01:00
7c252deede 📝 Update version to not specify minor version 2022-02-09 21:38:40 +01:00
Tadhg Boyle
224c5c4fe2 📝 Fix developers doc link (#133) 2022-02-09 21:33:38 +01:00
e945b5fe94 👌 Add version to PhpSwitcherDelegate 2022-02-09 21:32:55 +01:00
Tadhg Boyle
23e534c5c9 📝 Fix developers doc link (#133) 2022-02-09 21:31:50 +01:00
dcddf74830 🔧 New dev build 2022-02-08 23:51:16 +01:00
9baf69394e 🚩 Re-enabled "Restore Homebrew Permissions" 2022-02-08 23:50:45 +01:00
183 changed files with 8040 additions and 2793 deletions

33
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,33 @@
---
name: Bug report
about: Something going wrong? File a bug report!
title: ''
labels: bug
assignees: nicoverbruggen
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Required information**
- Did you consult the FAQ in the README? [yes/no]
- Did you try "Fix My Valet"? [yes/no]
- OS: [e.g. macOS Monterey]
- PHP Monitor version [e.g. v5.0.1]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,22 @@
---
name: Feature request
about: Suggest an enhancement.
title: ''
labels: enhancement
assignees: nicoverbruggen
---
_Enhancement requests that are not immediately approved will be moved to a discussion instead._
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

15
.swiftlint.yml Normal file
View File

@@ -0,0 +1,15 @@
disabled_rules:
- todo
- identifier_name
- force_try
- force_cast
opt_in_rules:
- empty_count
included:
- phpmon
- phpmon-tests
excluded:
- phpmon/Vendor

View File

@@ -1,5 +1,19 @@
# DEVELOPER README # DEVELOPER README
## ✅ Linting
This project uses the [SwiftLint](https://github.com/realm/SwiftLint) linter. You must install it and can run it like so:
```
swiftlint
```
It also automatically runs when you try to build the project. You'll get a warning if `swiftlint` is not installed, though. You can attempt to automatically fix issues:
```
swiftlint --fix
```
## 🔧 Build instructions ## 🔧 Build instructions
<img src="./docs/build.png" width="404px" alt="build button in Xcode"/> <img src="./docs/build.png" width="404px" alt="build button in Xcode"/>
@@ -13,6 +27,20 @@ Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive. If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
## 🚀 Release procedure
1. Merge into `main`
2. Create tag
3. Add changes to changelog + update security document
4. Archive
5. Notarize and prepare for own distribution
6. After notarization, export .app
7. Create zipped version
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
9. Upload to GitHub and add to tagged release
10. Update Cask with new version + hash
11. Check new version can be installed via Cask
## 🐛 Symbolication of crashes ## 🐛 Symbolication of crashes
If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs. If you have an archived build of the app and exported the DSYM, it is possible to symbolicate .ips crash logs.

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -61,6 +61,19 @@
ReferencedContainer = "container:PHP Monitor.xcodeproj"> ReferencedContainer = "container:PHP Monitor.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "--v"
isEnabled = "NO">
</CommandLineArgument>
</CommandLineArguments>
<EnvironmentVariables>
<EnvironmentVariable
key = "PHPMON_MARKETING_MODE"
value = "YES"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

108
README.md
View File

@@ -1,17 +1,12 @@
> If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project. > If this software has been useful to you, I ask that you **please star the repository**, that way I know that the software is being used. Also, please consider leaving [a one-time donation](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
> You can also send me [feedback](https://twitter.com/nicoverbruggen) if the app came in handy.<br>**Thank you!** ⭐️
<h1 align="center"><b>PHP Monitor</b> (phpmon)</h1> <p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
<p align="center"> **PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this app</u> (consult the FAQ below with info about how to set up your environment).
<img src="./phpmon/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png" alt="phpmon icon" width="128px" />
</p>
**PHP Monitor** (or *phpmon*) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It's tightly integrated with [Laravel Valet](https://github.com/laravel/valet), so <u>you need to have it set up before you can use this</u>. <img src="./docs/screenshot.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/>
<img src="./docs/screenshot50.jpg" width="1085px" alt="phpmon screenshot (menu bar app)"/> <small><i>Screenshot: Showing the key functionality of PHP Monitor.</i></small>
<small><i>Screenshot: Showing the key functionality of PHP Monitor. You can also add new domains as links, manage various services, and perform First Aid to fix all kinds of common PHP link issues.</i></small>
It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)! It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)!
@@ -19,16 +14,19 @@ It's super convenient to switch between different versions of PHP. You'll even g
PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more). PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more).
You can also add new domains as links, isolate sites, manage various services, and perform First Aid to fix all kinds of common PHP link issues.
## 🖥 System requirements ## 🖥 System requirements
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs. PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
* macOS 11 Big Sur or higher (supports macOS 12 Monterey) * macOS 11 Big Sur or higher (supports macOS 12 Monterey)
* Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew` * Homebrew is installed in `/usr/local/homebrew` or `/opt/homebrew`
* The brew formula `php` has to be installed (which version is detected) * Homebrew `php` formula is installed
* Laravel Valet 2.16.2 or higher (older versions might be compatible but are not supported) * Laravel Valet 2.16 or newer (supports Valet 3)
_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`._ _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._
## 🚀 How to install ## 🚀 How to install
@@ -43,7 +41,13 @@ To upgrade your existing installation, run:
brew upgrade phpmon brew upgrade phpmon
(You may need to run `brew update` first in order to update the cask file if you ran a Homebrew operation recently.) (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.)
## ⚡️ Launchers (Alfred, Raycast)
If you would like to integrate with your launcher of choice, you can also download an [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor) that works with PHP Monitor.
The app must be running in the background for these to work, and the _Allow third-party integrations_ checkbox must be enabled in Preferences (it is by default).
## 🔑 Is the app signed & notarized? ## 🔑 Is the app signed & notarized?
@@ -73,7 +77,7 @@ If you're still having issues, here's a few common questions & answers, as well
<summary><strong>Which versions of PHP are supported?</strong></summary> <summary><strong>Which versions of PHP are supported?</strong></summary>
<ul> <ul>
<li>PHP 5.6</li> <li>PHP 5.6 (only if you are running Valet 2)</li>
<li>PHP 7.0</li> <li>PHP 7.0</li>
<li>PHP 7.1</li> <li>PHP 7.1</li>
<li>PHP 7.2</li> <li>PHP 7.2</li>
@@ -84,7 +88,7 @@ If you're still having issues, here's a few common questions & answers, as well
<li>PHP 8.2 (experimental)</li> <li>PHP 8.2 (experimental)</li>
</ul> </ul>
For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported. 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.
</details> </details>
@@ -145,6 +149,38 @@ This should install `dnsmasq` and set up Valet. Great, almost there!
Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work. Finally, run PHP Monitor. Since the app is notarized and signed with a developer ID, it should work.
</details> </details>
<details>
<summary><strong>How frequently does PHP Monitor check for updates?</strong></summary>
PHP Monitor will check if an update is available every time you start the app.
You can disable this behaviour by going to Preferences (via the PHP Monitor icon in the menu bar) and unchecking "Automatically check for updates". You can always check for updates manually.
</details>
<details>
<summary><strong>I have PHP Monitor installed, and it works. I want to upgrade my PHP installations to the latest version, what's the best way to do this?</strong></summary>
It's easy to make a mistake here, and end up with an unlinked version of PHP or have versions missing from PHP Monitor.
Here's what I usually do:
* Open PHP Monitor and select **First Aid & Services** > **Restore Homebrew Permissions**.
* Close PHP Monitor after the pop-up tells you the permissions were restored.
* Run `brew update-reset`
* Run `brew upgrade`
If after this, any PHP versions are missing in PHP Monitor, please run the following for the versions that are missing:
* Run `brew uninstall php@x.x` (where `x.x` is the version)
* Run `brew cleanup` (if you get any permission issues you may need to manually clean up the folder)
* Run `brew install php@x.x` (where `x.x` is the version)
You may still need to run `brew link php` after upgrading, too.
That's it. Now start up PHP Monitor again and you should be golden!
</details>
<details> <details>
<summary><strong>PHP Monitor tells me `php` is not installed...</strong></summary> <summary><strong>PHP Monitor tells me `php` is not installed...</strong></summary>
@@ -197,6 +233,12 @@ You should see an error or a warning here in the output.
Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling). Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling).
</details> </details>
<details>
<summary><strong>The option to isolate a site is disabled! What's going on?</strong></summary>
Make sure you have at least **Valet 3.0** installed, since support for isolation was added in this version of Valet. (Please note that this version of Valet drops support for PHP 5.6.)
</details>
<details> <details>
<summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary> <summary><strong>One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark!</strong></summary>
@@ -231,16 +273,27 @@ Since v3.4 all of the loaded .ini files are sourced to determine which extension
<details> <details>
<summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary> <summary><strong>I've got two Homebrew installations on my Apple Silicon Mac, can I choose which installation to use with PHP Monitor?</strong></summary>
Not at this time, no. PHP Monitor will prefer the `/opt/homebrew` installation over the classic installation directory. If you are using PHP Monitor on an Intel machine or on an Apple Silicon machine with Rosetta enabled, PHP Monitor expects the main Homebrew binary in `/usr/local/bin/brew`.
If you are using PHP Monitor on Apple Silicon without Rosetta, PHP Monitor expects the main Homebrew binary in `/opt/homebrew/bin/brew`.
If there's an issue here, you'll get an alert at launch.
Make sure that the version of Homebrew that you are running normally is the same as the one that PHP Monitor expects. If you are on M1 hardware for example, but still using Rosetta for Homebrew, you'll need to run PHP Monitor under Rosetta as well.
PHP Monitor is a universal app and supports both architectures, so [find out here](https://support.apple.com/en-us/HT211861) how to enable Rosetta with PHP Monitor.
</details> </details>
<details> <details>
<summary><strong>Why is the app doing network requests?</strong></summary> <summary><strong>Why is the app doing network requests?</strong></summary>
It's Homebrew. I can't prevent `brew` from doing things via the network when I invoke it. The app will automatically check for updates, which is the most likely culprit.
PHP Monitor itself doesn't do any network requests. Feel free to check the source code or intercept the traffic, if you don't believe me. This happens at launch (unless disabled), and the app directly checks the Caskfile hosted on GitHub. This data is not, and will not be used for analytics (and, as far as I can tell, cannot).
I also can't prevent `brew` from doing things via the network when PHP Monitor uses the binary.
The app includes an Internet Access Policy file, so if you're using something like Little Snitch there should be a description why these calls occur.
</details> </details>
@@ -267,9 +320,11 @@ You can put as many apps as you'd like in the `scan_apps` array, and PHP Monitor
</details> </details>
<details> <details>
<summary><strong>How can the app integrate with third party tools, like Alfred?</strong></summary> <summary><strong>How can the app integrate with third party tools, like Alfred or Raycast?</strong></summary>
There's an Alfred workflow usually included in the release list, you can grab it by going to releases and downloading the asset `phpmon.alfredworkflow`. PHP Monitor supports third party app integrations by default, and this feature is enabled in Preferences unless you disable it.
You can grab the official [Alfred workflow](https://github.com/nicoverbruggen/phpmon/raw/main/integrations/phpmon.alfredworkflow) or [Raycast extension](https://www.raycast.com/nicoverbruggen/php-monitor).
If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor). If you'd like to integrate something yourself, all you need to to is use the `phpmon://` protocol and ensure that third party app integrations are enabled in Preferences (in PHP Monitor).
@@ -351,13 +406,14 @@ Donations really help with the Apple Developer Program cost, and keep me motivat
## 😎 Acknowledgements ## 😎 Acknowledgements
While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks: While I did make this application during my own free time, PHP Monitor started out from various learning experiments during work hours at my employer, DIVE. I'd also like to shout out the following folks:
* My colleagues at [DIVE](https://dive.be) * My colleagues at [DIVE](https://dive.be)
* The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors) * The [Homebrew](https://brew.sh/) team & [Valet maintainers](https://github.com/laravel/valet/graphs/contributors)
* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so * Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) when PHP Monitor was still very much a small app with a handful of stars or so
* My [GitHub Sponsors](https://github.com/sponsors/nicoverbruggen) and those who have donated
* Everyone who has left feedback and reported bugs (appreciate it!)
* Everyone in the Laravel community who shared the app (thanks!) * Everyone in the Laravel community who shared the app (thanks!)
* Everyone who left feedback via issues & who donated to keep the project up and running
Thank you very much for your contributions, kind words and support. Thank you very much for your contributions, kind words and support.
@@ -373,7 +429,9 @@ In order to save power, this only happens once every 60 seconds.
This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them. This utility will detect which PHP versions you have installed via Homebrew, and then allows you to switch between them.
The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be much faster than Valets switcher. The switcher will disable all PHP-FPM services not belonging to the version you wish to use, and link the desired version of PHP. Then, it'll restart your desired PHP version's FPM process. This all happens in parallel, so this should be a bit faster than Valets switcher.
If you're using Valet 3, versions of PHP-FPM required to keep isolated sites up and running will also be started or stopped as needed.
### Config change detection ### Config change detection
@@ -399,4 +457,4 @@ I have done my best to annotate as much as humanly possible, and have avoided us
I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests. I also have a few tests for key parts of the application that I found needed to be tested. In the future, I would like to add even more tests for some of the UI stuff, but for now the tests are more unit tests than feature tests.
For more detailed information for developers, please see [the documentation file for developers](./DEVS.md). For more detailed information for developers, please see [the documentation file for developers](./DEVELOPER.md).

View File

@@ -1,13 +0,0 @@
# Release Procedure
1. Merge into `main`
2. Create tag
3. Add changes to changelog + update security document
4. Archive
5. Notarize and prepare for own distribution
6. After notarization, export .app
7. Create zipped version
8. Calculate SHA256: `openssl dgst -sha256 phpmon.zip`
9. Upload to GitHub and add to tagged release
10. Update Cask with new version + hash
11. Check new version can be installed via Cask

View File

@@ -4,9 +4,11 @@
Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up): Generally speaking, only the latest version of **PHP Monitor** is supported, except during transition periods (for example, when particular system requirements go up):
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version | | Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Recommended Valet Version |
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ---- | ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
| 5.0 | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 | | 5.x | ✅ Universal binary | ✅ Yes | Big Sur (11.0) and Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 (*) | 3.0 (2.16.2 minimum) |
_(*) Support for PHP 5.6 is only included if you are using Valet 2.x, since support for PHP 5.6 was dropped in Valet 3.0._
## Legacy versions ## Legacy versions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
docs/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

View File

@@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 13/02/2021. // Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@@ -15,11 +15,11 @@ class CommandTest: XCTestCase {
path: Paths.php, path: Paths.php,
arguments: ["-v"] arguments: ["-v"]
) )
XCTAssert(version.contains("(cli)")) XCTAssert(version.contains("(cli)"))
XCTAssert(version.contains("NTS")) XCTAssert(version.contains("NTS"))
XCTAssert(version.contains("built")) XCTAssert(version.contains("built"))
XCTAssert(version.contains("Zend")) XCTAssert(version.contains("Zend"))
} }
} }

View File

@@ -3,18 +3,18 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 14/02/2021. // Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
class BrewJsonParserTest: XCTestCase { class HomebrewPackageTest: XCTestCase {
// - MARK: SYNTHETIC TESTS // - MARK: SYNTHETIC TESTS
static var jsonBrewFile: URL { static var jsonBrewFile: URL {
return Bundle(for: Self.self) return Bundle(for: Self.self)
.url(forResource: "brew", withExtension: "json")! .url(forResource: "brew-formula", withExtension: "json")!
} }
func testCanLoadExtensionJson() throws { func testCanLoadExtensionJson() throws {
@@ -22,7 +22,7 @@ class BrewJsonParserTest: XCTestCase {
let package = try! JSONDecoder().decode( let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json.data(using: .utf8)! [HomebrewPackage].self, from: json.data(using: .utf8)!
).first! ).first!
XCTAssertEqual(package.name, "php") XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php") XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.0") XCTAssertEqual(package.aliases.first!, "php@8.0")
@@ -30,23 +30,23 @@ class BrewJsonParserTest: XCTestCase {
installed.version.starts(with: "8.0") installed.version.starts(with: "8.0")
}), true) }), true)
} }
static var jsonBrewServicesFile: URL { static var jsonBrewServicesFile: URL {
return Bundle(for: Self.self) return Bundle(for: Self.self)
.url(forResource: "brew-services", withExtension: "json")! .url(forResource: "brew-services", withExtension: "json")!
} }
func testCanParseServicesJson() throws { func testCanParseServicesJson() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8) let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode( let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)! [HomebrewService].self, from: json.data(using: .utf8)!
) )
XCTAssertGreaterThan(services.count, 0) XCTAssertGreaterThan(services.count, 0)
XCTAssertEqual(services.first?.name, "dnsmasq") XCTAssertEqual(services.first?.name, "dnsmasq")
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq") XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
} }
// - MARK: LIVE TESTS // - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up, /// This test requires that you have a valid Homebrew installation set up,
@@ -63,13 +63,13 @@ class BrewJsonParserTest: XCTestCase {
).filter({ service in ).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name) return ["php", "nginx", "dnsmasq"].contains(service.name)
}) })
XCTAssertTrue(services.contains(where: {$0.name == "php"} )) XCTAssertTrue(services.contains(where: {$0.name == "php"}))
XCTAssertTrue(services.contains(where: {$0.name == "nginx"} )) XCTAssertTrue(services.contains(where: {$0.name == "nginx"}))
XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"} )) XCTAssertTrue(services.contains(where: {$0.name == "dnsmasq"}))
XCTAssertEqual(services.count, 3) XCTAssertEqual(services.count, 3)
} }
/// This test requires that you have a valid Homebrew installation set up, /// This test requires that you have a valid Homebrew installation set up,
/// and requires the `php` formula to be installed. /// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation /// If this test fails, there is an issue with your Homebrew installation
@@ -79,7 +79,7 @@ class BrewJsonParserTest: XCTestCase {
[HomebrewPackage].self, [HomebrewPackage].self,
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)! from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
).first! ).first!
XCTAssertTrue(package.name == "php") XCTAssertTrue(package.name == "php")
} }
} }

View File

@@ -0,0 +1,81 @@
//
// NginxConfigurationTest.swift
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import XCTest
class NginxConfigurationTest: XCTestCase {
// MARK: - Test Files
static var regularUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site", withExtension: "test")!
}
static var isolatedUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-site-isolated", withExtension: "test")!
}
static var proxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-proxy", withExtension: "test")!
}
static var secureProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy", withExtension: "test")!
}
static var customTldProxyUrl: URL {
return Bundle(for: Self.self).url(forResource: "nginx-secure-proxy-custom-tld", withExtension: "test")!
}
// MARK: - Tests
func testCanDetermineSiteNameAndTld() throws {
XCTAssertEqual(
"nginx-site",
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
)
XCTAssertEqual(
"test",
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld
)
}
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
)
XCTAssertEqual(
"8.1",
NginxConfiguration.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion
)
}
func testCanDetermineProxy() throws {
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.proxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
let normal = NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)!
XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual(nil, normal.proxy)
}
func testCanDetermineSecuredProxy() throws {
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.secureProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
}
func testCanDetermineProxyWithCustomTld() throws {
let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://localhost:8080", proxied.proxy)
}
}

View File

@@ -3,30 +3,30 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 13/02/2021. // Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
class ExtensionParserTest: XCTestCase { class PhpExtensionTest: XCTestCase {
static var phpIniFileUrl: URL { static var phpIniFileUrl: URL {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")! return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
} }
func testCanLoadExtension() throws { func testCanLoadExtension() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
XCTAssertGreaterThan(extensions.count, 0) XCTAssertGreaterThan(extensions.count, 0)
} }
func testExtensionNameIsCorrect() throws { func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
let extensionNames = extensions.map { (ext) -> String in let extensionNames = extensions.map { (ext) -> String in
return ext.name return ext.name
} }
// These 6 should be found // These 6 should be found
XCTAssertTrue(extensionNames.contains("xdebug")) XCTAssertTrue(extensionNames.contains("xdebug"))
XCTAssertTrue(extensionNames.contains("imagick")) XCTAssertTrue(extensionNames.contains("imagick"))
@@ -34,39 +34,44 @@ class ExtensionParserTest: XCTestCase {
XCTAssertTrue(extensionNames.contains("opcache")) XCTAssertTrue(extensionNames.contains("opcache"))
XCTAssertTrue(extensionNames.contains("yaml")) XCTAssertTrue(extensionNames.contains("yaml"))
XCTAssertTrue(extensionNames.contains("custom")) XCTAssertTrue(extensionNames.contains("custom"))
XCTAssertFalse(extensionNames.contains("fake")) XCTAssertFalse(extensionNames.contains("fake"))
XCTAssertFalse(extensionNames.contains("nice")) XCTAssertFalse(extensionNames.contains("nice"))
} }
func testExtensionStatusIsCorrect() throws { func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.load(from: Self.phpIniFileUrl) let extensions = PhpExtension.load(from: Self.phpIniFileUrl)
// xdebug should be enabled // xdebug should be enabled
XCTAssertEqual(extensions[0].enabled, true) XCTAssertEqual(extensions[0].enabled, true)
// imagick should be disabled // imagick should be disabled
XCTAssertEqual(extensions[1].enabled, false) XCTAssertEqual(extensions[1].enabled, false)
} }
func testToggleWorksAsExpected() throws { func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.load(from: destination) let extensions = PhpExtension.load(from: destination)
XCTAssertEqual(extensions.count, 6) XCTAssertEqual(extensions.count, 6)
// Try to disable xdebug (should be detected first)! // Try to disable xdebug (should be detected first)!
let xdebug = extensions.first! let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug") XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true) XCTAssertEqual(xdebug.enabled, true)
xdebug.toggle() xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false) XCTAssertEqual(xdebug.enabled, false)
// Check if the file contains the appropriate data // Check if the file contains the appropriate data
let file = try! String(contentsOf: destination, encoding: .utf8) let file = try! String(contentsOf: destination, encoding: .utf8)
XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\"")) XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\""))
// Make sure if we load the data again, it's disabled // Make sure if we load the data again, it's disabled
XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false) XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false)
} }
func testCanRetrieveXdebugMode() throws {
let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
XCTAssertEqual(value, "coverage")
}
} }

View File

@@ -3,20 +3,20 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 29/11/2021. // Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
class ValetConfigParserTest: XCTestCase { class ValetConfigurationTest: XCTestCase {
static var jsonConfigFileUrl: URL { static var jsonConfigFileUrl: URL {
return Bundle(for: Self.self).url( return Bundle(for: Self.self).url(
forResource: "valet-config", forResource: "valet-config",
withExtension: "json" withExtension: "json"
)! )!
} }
func testCanLoadConfigFile() throws { func testCanLoadConfigFile() throws {
let json = try? String( let json = try? String(
contentsOf: Self.jsonConfigFileUrl, contentsOf: Self.jsonConfigFileUrl,
@@ -26,13 +26,14 @@ class ValetConfigParserTest: XCTestCase {
Valet.Configuration.self, Valet.Configuration.self,
from: json!.data(using: .utf8)! from: json!.data(using: .utf8)!
) )
XCTAssertEqual(config.tld, "test") XCTAssertEqual(config.tld, "test")
XCTAssertEqual(config.paths, [ XCTAssertEqual(config.paths, [
"/Users/username/.config/valet/Sites", "/Users/username/.config/valet/Sites",
"/Users/username/Sites" "/Users/username/Sites"
]) ])
XCTAssertEqual(config.defaultSite, "/Users/username/default-site")
XCTAssertEqual(config.loopback, "127.0.0.1") XCTAssertEqual(config.loopback, "127.0.0.1")
} }
} }

View File

@@ -0,0 +1,81 @@
# valet stub: proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -0,0 +1,57 @@
# valet stub: secure.proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name live.whatagraph.dev.com www.live.whatagraph.dev.com *.live.whatagraph.dev.com;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.crt";
ssl_certificate_key "/Users/phpmon/.config/valet/Certificates/live.whatagraph.dev.com.key";
access_log off;
error_log "/Users/phpmon/.config/valet/Log/live.whatagraph.dev.com-error.log";
error_page 404 "/Users/phpmon/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://localhost:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -0,0 +1,57 @@
# valet stub: secure.proxy.valet.conf
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name my-proxy.test www.my-proxy.test *.my-proxy.test;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/my-proxy.test.key";
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/my-proxy.test-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location / {
proxy_pass http://127.0.0.1:90;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -0,0 +1,94 @@
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 512M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.key";
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# ISOLATED_PHP_VERSION=php@8.1
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet81.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -0,0 +1,93 @@
server {
listen 127.0.0.1:80;
#listen 127.0.0.1:80; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
#listen 127.0.0.1:443 ssl http2; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 512M;
http2_push_preload on;
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.crt";
ssl_certificate_key "/Users/nicoverbruggen/.config/valet/Certificates/nicoverbruggen.test.key";
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
#listen 127.0.0.1:60; # valet loopback
server_name nicoverbruggen.test www.nicoverbruggen.test *.nicoverbruggen.test;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /41c270e4-5535-4daa-b23e-c269744c2f45/ {
internal;
alias /;
try_files $uri $uri/;
}
location / {
rewrite ^ "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php" last;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "/Users/nicoverbruggen/.config/valet/Log/nginx-error.log";
error_page 404 "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass "unix:/Users/nicoverbruggen/.config/valet/valet.sock";
fastcgi_index "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME "/Users/nicoverbruggen/.composer/vendor/laravel/valet/server.php";
fastcgi_param PATH_INFO $fastcgi_path_info;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -4,5 +4,6 @@
"/Users/username/.config/valet/Sites", "/Users/username/.config/valet/Sites",
"/Users/username/Sites" "/Users/username/Sites"
], ],
"loopback": "127.0.0.1" "loopback": "127.0.0.1",
"default": "/Users/username/default-site"
} }

View File

@@ -3,18 +3,18 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 14/02/2021. // Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class Utility { class Utility {
public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? { public static func copyToTemporaryFile(resourceName: String, fileExtension: String) -> URL? {
if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) { if let bundleURL = Bundle(for: Self.self).url(forResource: resourceName, withExtension: fileExtension) {
let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true) let tempDirectoryURL = NSURL.fileURL(withPath: NSTemporaryDirectory(), isDirectory: true)
let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)") let targetURL = tempDirectoryURL.appendingPathComponent("\(UUID().uuidString).\(fileExtension)")
do { do {
try FileManager.default.copyItem(at: bundleURL, to: targetURL) try FileManager.default.copyItem(at: bundleURL, to: targetURL)
return targetURL return targetURL
@@ -22,7 +22,7 @@ class Utility {
Log.err("Unable to copy file: \(error)") Log.err("Unable to copy file: \(error)")
} }
} }
return nil return nil
} }
} }

View File

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

View File

@@ -0,0 +1,21 @@
//
// 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)
}
}

View File

@@ -0,0 +1,62 @@
//
// 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

@@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 01/04/2021. // Created by Nico Verbruggen on 01/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@@ -22,8 +22,8 @@ class PhpVersionDetectionTest: XCTestCase {
"unrelatedphp@1.0", // should be omitted, invalid "unrelatedphp@1.0", // should be omitted, invalid
"php@5.6", "php@5.6",
"php@5.4" // should be omitted, not supported "php@5.4" // should be omitted, not supported
], checkBinaries: false) ], checkBinaries: false, generateHelpers: false)
XCTAssertEqual(outcome, ["8.0", "7.0", "5.6"]) XCTAssertEqual(outcome, ["8.0", "7.0"])
} }
} }

View File

@@ -11,20 +11,24 @@ import XCTest
class PhpVersionNumberTest: XCTestCase { class PhpVersionNumberTest: XCTestCase {
func testCanDeconstructPhpVersion() throws { func testCanDeconstructPhpVersion() throws {
XCTAssertEqual(
try! PhpVersionNumber.parse("PHP 8.2.0-dev"),
PhpVersionNumber(major: 8, minor: 2, patch: 0)
)
XCTAssertEqual( XCTAssertEqual(
try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"), try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"),
PhpVersionNumber(major: 8, minor: 1, patch: 0) PhpVersionNumber(major: 8, minor: 1, patch: 0)
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumber.make(from: "8.0.11"), try! PhpVersionNumber.parse("8.0.11"),
PhpVersionNumber(major: 8, minor: 0, patch: 11) PhpVersionNumber(major: 8, minor: 0, patch: 11)
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumber.make(from: "7.4.2"), try! PhpVersionNumber.parse("7.4.2"),
PhpVersionNumber(major: 7, minor: 4, patch: 2) PhpVersionNumber(major: 7, minor: 4, patch: 2)
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumber.make(from: "7.4"), try! PhpVersionNumber.parse("7.4"),
PhpVersionNumber(major: 7, minor: 4, patch: nil) PhpVersionNumber(major: 7, minor: 4, patch: nil)
) )
XCTAssertEqual( XCTAssertEqual(
@@ -32,13 +36,13 @@ class PhpVersionNumberTest: XCTestCase {
nil nil
) )
} }
func testPhpVersionNumberParse() throws { func testPhpVersionNumberParse() throws {
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
XCTAssertTrue(error is VersionParseError) XCTAssertTrue(error is VersionParseError)
} }
} }
func testCanCheckFixedConstraints() throws { func testCanCheckFixedConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -47,7 +51,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"]) .make(from: ["7.4.3", "7.3.3", "7.2.3", "7.1.3", "7.0.3"])
@@ -55,7 +59,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0.3"]).all .make(from: ["7.0.3"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -63,7 +67,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -72,7 +76,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: []).all .make(from: []).all
) )
} }
func testCanCheckCaretConstraints() throws { func testCanCheckCaretConstraints() throws {
// 1. Imprecise checks // 1. Imprecise checks
XCTAssertEqual( XCTAssertEqual(
@@ -82,7 +86,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 2. Imprecise check with precise constraint (lenient AKA not strict) // 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc. // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -92,7 +96,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 3. Imprecise check with precise constraint (strict mode) // 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -102,7 +106,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all .make(from: ["7.4", "7.3", "7.2", "7.1"]).all
) )
// 4. Precise members and constraint all around // 4. Precise members and constraint all around
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -111,7 +115,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
// 5. Precise members but imprecise constraint (strict mode) // 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0 // In strict mode the constraint's patch version is assumed to be 0
XCTAssertEqual( XCTAssertEqual(
@@ -121,7 +125,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
// 6. Precise members but imprecise constraint (lenient mode) // 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal // In lenient mode the constraint's patch version is assumed to be equal
XCTAssertEqual( XCTAssertEqual(
@@ -132,7 +136,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
} }
func testCanCheckTildeConstraints() throws { func testCanCheckTildeConstraints() throws {
// 1. Imprecise checks // 1. Imprecise checks
XCTAssertEqual( XCTAssertEqual(
@@ -142,7 +146,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// 2. Imprecise check with precise constraint (lenient AKA not strict) // 2. Imprecise check with precise constraint (lenient AKA not strict)
// These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc. // These versions are interpreted as 7.4.999, 7.3.999, 7.2.999, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -155,7 +159,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0"]).all .make(from: ["7.0"]).all
) )
// 3. Imprecise check with precise constraint (strict mode) // 3. Imprecise check with precise constraint (strict mode)
// These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc. // These versions are interpreted as 7.4.0, 7.3.0, 7.2.0, etc.
XCTAssertEqual( XCTAssertEqual(
@@ -168,7 +172,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: []).all .make(from: []).all
) )
// 4. Precise members and constraint all around // 4. Precise members and constraint all around
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -179,7 +183,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.0.10"]).all .make(from: ["7.0.10"]).all
) )
// 5. Precise members but imprecise constraint (strict mode) // 5. Precise members but imprecise constraint (strict mode)
// In strict mode the constraint's patch version is assumed to be 0. // In strict mode the constraint's patch version is assumed to be 0.
XCTAssertEqual( XCTAssertEqual(
@@ -189,7 +193,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
// 6. Precise members but imprecise constraint (lenient mode) // 6. Precise members but imprecise constraint (lenient mode)
// In lenient mode the constraint's patch version is assumed to be equal. // In lenient mode the constraint's patch version is assumed to be equal.
// (Strictness does not make any difference here, but both should be tested.) // (Strictness does not make any difference here, but both should be tested.)
@@ -201,7 +205,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all .make(from: ["7.4.10", "7.3.10", "7.2.10", "7.1.10", "7.0.10"]).all
) )
} }
func testCanCheckGreaterThanOrEqualConstraints() throws { func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -210,7 +214,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -218,7 +222,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]).all
) )
// Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0) // Strict check (>7.2.5 is too new for 7.2 which resolves to 7.2.0)
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -227,7 +231,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all .make(from: ["7.4", "7.3"]).all
) )
// Non-strict check (ignoring patch, 7.2 resolves to 7.2.999) // Non-strict check (ignoring patch, 7.2 resolves to 7.2.999)
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -237,7 +241,7 @@ class PhpVersionNumberTest: XCTestCase {
.make(from: ["7.4", "7.3", "7.2"]).all .make(from: ["7.4", "7.3", "7.2"]).all
) )
} }
func testCanCheckGreaterThanConstraints() throws { func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
@@ -246,7 +250,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1"]).all .make(from: ["7.4", "7.3", "7.2", "7.1"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -255,7 +259,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2"]).all .make(from: ["7.4", "7.3", "7.2"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"]) .make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@@ -264,7 +268,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.4", "7.3"]).all .make(from: ["7.4", "7.3"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])
@@ -273,7 +277,7 @@ class PhpVersionNumberTest: XCTestCase {
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2"]).all .make(from: ["7.3.1", "7.2.9", "7.2"]).all
) )
XCTAssertEqual( XCTAssertEqual(
PhpVersionNumberCollection PhpVersionNumberCollection
.make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"]) .make(from: ["7.3.1", "7.2.9", "7.2.8", "7.2.6", "7.2.5", "7.2"])

View File

@@ -0,0 +1,18 @@
//
// 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

@@ -3,7 +3,7 @@
// phpmon-tests // phpmon-tests
// //
// Created by Nico Verbruggen on 16/12/2021. // Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import XCTest import XCTest
@@ -14,12 +14,12 @@ class VersionExtractorTest: XCTestCase {
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1") XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0") XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
} }
func testVersionComparison() { func testVersionComparison() {
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending) XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending) XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame) XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)
XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending) XCTAssertEqual("2.17.0".versionCompare("2.17.1"), .orderedAscending)
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "Default.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Default@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 861 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "Proxy.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Proxy@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 935 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,25 @@
{
"images" : [
{
"filename" : "Isolated.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "Isolated@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 827 B

View File

@@ -2,145 +2,124 @@
// Services.swift // Services.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
import AppKit import AppKit
class Actions { class Actions {
// MARK: - Services // MARK: - Services
public static func restartPhpFpm() public static func restartPhpFpm() {
{
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true) brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
} }
public static func restartNginx() public static func restartNginx() {
{
brew("services restart nginx", sudo: true) brew("services restart nginx", sudo: true)
} }
public static func restartDnsMasq() public static func restartDnsMasq() {
{
brew("services restart dnsmasq", sudo: true) brew("services restart dnsmasq", sudo: true)
} }
public static func stopAllServices() public static func stopAllServices() {
{
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true) brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true) brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true) brew("services stop dnsmasq", sudo: true)
} }
public static func fixHomebrewPermissions() throws public static func fixHomebrewPermissions() throws {
{
var servicesCommands = [ var servicesCommands = [
"\(Paths.brew) services stop nginx", "\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq", "\(Paths.brew) services stop dnsmasq"
] ]
var cellarCommands = [ var cellarCommands = [
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/nginx", "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):staff \(Paths.cellarPath)/dnsmasq" "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
] ]
PhpEnv.shared.availablePhpVersions.forEach { version in PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpVersion let formula = version == PhpEnv.brewPhpVersion
? "php" ? "php"
: "php@\(version)" : "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)") servicesCommands.append("\(Paths.brew) services stop \(formula)")
cellarCommands.append("chown -R \(Paths.whoami):staff \(Paths.cellarPath)/\(formula)") cellarCommands.append("chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(formula)")
} }
let script = let script =
servicesCommands.joined(separator: " && ") servicesCommands.joined(separator: " && ")
+ " && " + " && "
+ cellarCommands.joined(separator: " && ") + cellarCommands.joined(separator: " && ")
let appleScript = NSAppleScript( let appleScript = NSAppleScript(
source: "do shell script \"\(script)\" with administrator privileges" source: "do shell script \"\(script)\" with administrator privileges"
) )
let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil) let eventResult: NSAppleEventDescriptor? = appleScript?.executeAndReturnError(nil)
if (eventResult == nil) { if eventResult == nil {
throw HomebrewPermissionError(kind: .applescriptNilError) throw HomebrewPermissionError(kind: .applescriptNilError)
} }
} }
// MARK: - Finding Config Files // MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() public static func openGenericPhpConfigFolder() {
{ let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")]
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
} }
public static func openGlobalComposerFolder() public static func openGlobalComposerFolder() {
{
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json") .appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
} }
public static func openPhpConfigFolder(version: String) public static func openPhpConfigFolder(version: String) {
{ let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")];
NSWorkspace.shared.activateFileViewerSelecting(files as [URL]) NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
} }
public static func openValetConfigFolder() public static func openValetConfigFolder() {
{
let file = FileManager.default.homeDirectoryForCurrentUser let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet") .appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL]) NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
} }
// MARK: - Other Actions // MARK: - Other Actions
public static func createTempPhpInfoFile() -> URL public static func createTempPhpInfoFile() -> URL {
{
// Write a file called `phpmon_phpinfo.php` to /tmp // Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8) try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file // 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") Shell.run("\(Paths.binPath)/php-cgi -q /tmp/phpmon_phpinfo.php > /tmp/phpmon_phpinfo.html")
return URL(string: "file:///private/tmp/phpmon_phpinfo.html")! return URL(string: "file:///private/tmp/phpmon_phpinfo.html")!
} }
// MARK: - Fix My Valet // MARK: - Fix My Valet
/** /**
Detects all currently available PHP versions, Detects all currently available PHP versions,
and unlinks each and every one of them. and unlinks each and every one of them.
This all happens in sequence, nothing runs in parallel.
After this, the brew services are also stopped, After this, the brew services are also stopped,
the latest PHP version is linked, and php + nginx are restarted. the latest PHP version is linked, and php + nginx are restarted.
If this does not solve the issue, the user may need to install additional If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`. extensions and/or run `composer global update`.
*/ */
public static func fixMyValet() public static func fixMyValet(completed: @escaping () -> Void) {
{ InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true) brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)
PhpEnv.shared.detectPhpVersions().forEach { (version) in brew("services restart nginx", sudo: true)
let formula = (version == PhpEnv.brewPhpVersion) ? "php" : "php@\(version)" completed()
brew("unlink php@\(version)") })
brew("services stop \(formula)")
brew("services stop \(formula)", sudo: true)
}
brew("services stop dnsmasq")
brew("services stop php")
brew("services stop nginx")
brew("link php --overwrite --force")
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)
brew("services restart nginx", sudo: true)
} }
} }

View File

@@ -2,13 +2,13 @@
// Command.swift // Command.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
public class Command { public class Command {
/** /**
Immediately executes a command. Immediately executes a command.
@@ -20,21 +20,21 @@ public class Command {
let task = Process() let task = Process()
task.launchPath = path task.launchPath = path
task.arguments = arguments task.arguments = arguments
let pipe = Pipe() let pipe = Pipe()
task.standardOutput = pipe task.standardOutput = pipe
task.launch() task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile() let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = String.init(data: data, encoding: String.Encoding.utf8)! let output: String = String.init(data: data, encoding: String.Encoding.utf8)!
if (trimNewlines) { if trimNewlines {
return output.components(separatedBy: .newlines) return output.components(separatedBy: .newlines)
.filter({ !$0.isEmpty }) .filter({ !$0.isEmpty })
.joined(separator: "\n") .joined(separator: "\n")
} }
return output return output
} }
} }

View File

@@ -2,19 +2,19 @@
// Constants.swift // Constants.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class Constants { struct Constants {
/** /**
* The latest PHP version that is considered to be stable at the time of release. * 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). * This version number is currently not used (only as a default fallback).
*/ */
static let LatestStablePhpVersion = "8.1" static let LatestStablePhpVersion = "8.1"
/** /**
The minimum version of Valet that is recommended. The minimum version of Valet that is recommended.
If the installed version is older, a notification will be shown If the installed version is older, a notification will be shown
@@ -24,7 +24,7 @@ class Constants {
See also: https://github.com/laravel/valet/releases/tag/v2.16.2 See also: https://github.com/laravel/valet/releases/tag/v2.16.2
*/ */
static let MinimumRecommendedValetVersion = "2.16.2" static let MinimumRecommendedValetVersion = "2.16.2"
/** /**
* The PHP versions supported by this application. * The PHP versions supported by this application.
* Versions that do not appear in this array are omitted from the list. * Versions that do not appear in this array are omitted from the list.
@@ -34,7 +34,7 @@ class Constants {
// STABLE RELEASES // STABLE RELEASES
// ==================== // ====================
// Versions of PHP that are stable and are supported. // Versions of PHP that are stable and are supported.
"5.6", "5.6", // only supported when Valet 2.x is active
"7.0", "7.0",
"7.1", "7.1",
"7.2", "7.2",
@@ -42,7 +42,7 @@ class Constants {
"7.4", "7.4",
"8.0", "8.0",
"8.1", "8.1",
// ==================== // ====================
// EXPERIMENTAL SUPPORT // EXPERIMENTAL SUPPORT
// ==================== // ====================
@@ -51,9 +51,32 @@ class Constants {
"8.2" "8.2"
] ]
/** struct Urls {
The URL that people can visit if they wish to help support the project.
*/ static let DonationPayment = URL(
static let DonationUrl = URL(string: "https://nicoverbruggen.be/sponsor#pay-now")! string: "https://nicoverbruggen.be/sponsor#pay-now"
)!
static let DonationPage = URL(
string: "https://nicoverbruggen.be/sponsor"
)!
static let FrequentlyAskedQuestions = URL(
string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting"
)!
static let GitHubReleases = URL(
string: "https://github.com/nicoverbruggen/phpmon/releases"
)!
static let StableBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb"
)!
static let DevBuildCaskFile = URL(
string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon-dev.rb"
)!
}
} }

View File

@@ -9,7 +9,7 @@
import Foundation import Foundation
class Events { class Events {
static let ServicesUpdated = Notification.Name("ServicesUpdated") static let ServicesUpdated = Notification.Name("ServicesUpdated")
} }

View File

@@ -3,39 +3,36 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 24/12/2021. // Created by Nico Verbruggen on 24/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
// MARK: Common Shell Commands // MARK: Common Shell Commands
/** /**
Runs a `valet` command. Runs a `valet` command. Defaults to running as superuser.
*/ */
func valet(_ command: String) -> String func valet(_ command: String, sudo: Bool = true) -> String {
{ return Shell.pipe("\(sudo ? "sudo " : "")" + "\(Paths.valet) \(command)", requiresPath: true)
return Shell.pipe("sudo \(Paths.valet) \(command)", requiresPath: true)
} }
/** /**
Runs a `brew` command. Can run as superuser. Runs a `brew` command. Can run as superuser.
*/ */
func brew(_ command: String, sudo: Bool = false) func brew(_ command: String, sudo: Bool = false) {
{
Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)") Shell.run("\(sudo ? "sudo " : "")" + "\(Paths.brew) \(command)")
} }
/** /**
Runs `sed` in order to replace all occurrences of a string in a specific file with another. 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) {
{
// Escape slashes (or `sed` won't work) // Escape slashes (or `sed` won't work)
let e_original = original.replacingOccurrences(of: "/", with: "\\/") let e_original = original.replacingOccurrences(of: "/", with: "\\/")
let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/") let e_replacement = replacement.replacingOccurrences(of: "/", with: "\\/")
// Check if gsed exists; it is able to follow symlinks, // Check if gsed exists; it is able to follow symlinks,
// which we want to do to toggle the extension // which we want to do to toggle the extension
if Shell.fileExists("\(Paths.binPath)/gsed") { if Filesystem.fileExists("\(Paths.binPath)/gsed") {
Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)") Shell.run("\(Paths.binPath)/gsed -i --follow-symlinks 's/\(e_original)/\(e_replacement)/g' \(file)")
} else { } else {
Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)") Shell.run("sed -i '' 's/\(e_original)/\(e_replacement)/g' \(file)")
@@ -45,8 +42,7 @@ func sed(file: String, original: String, replacement: String)
/** /**
Uses `grep` to determine whether a particular query string can be found in a particular file. Uses `grep` to determine whether a particular query string can be found in a particular file.
*/ */
func grepContains(file: String, query: String) -> Bool func grepContains(file: String, query: String) -> Bool {
{
return Shell.pipe(""" return Shell.pipe("""
grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO" grep -q '\(query)' \(file); [ $? -eq 0 ] && echo "YES" || echo "NO"
""") """)

View File

@@ -3,50 +3,56 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 21/12/2021. // Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class Log { class Log {
static var shared = Log() static var shared = Log()
enum Verbosity: Int { enum Verbosity: Int {
case error = 1, case error = 1,
warning = 2, warning = 2,
info = 3, info = 3,
performance = 4 performance = 4
public func isApplicable() -> Bool { public func isApplicable() -> Bool {
return Log.shared.verbosity.rawValue >= self.rawValue return Log.shared.verbosity.rawValue >= self.rawValue
} }
} }
var verbosity: Verbosity = .warning var verbosity: Verbosity = .warning
static func err(_ item: Any) { static func err(_ item: Any) {
if Verbosity.error.isApplicable() { if Verbosity.error.isApplicable() {
print("[ERR] \(item)") print("[E] \(item)")
} }
} }
static func warn(_ item: Any) { static func warn(_ item: Any) {
if Verbosity.warning.isApplicable() { if Verbosity.warning.isApplicable() {
print("[WRN] \(item)") print("[W] \(item)")
} }
} }
static func info(_ item: Any) { static func info(_ item: Any) {
if Verbosity.info.isApplicable() { if Verbosity.info.isApplicable() {
print(item) print("\(item)")
} }
} }
static func perf(_ item: Any) { static func perf(_ item: Any) {
if Verbosity.performance.isApplicable() { if Verbosity.performance.isApplicable() {
print(item) print("[P] \(item)")
} }
} }
static func separator(as verbosity: Verbosity = .info) {
if verbosity.isApplicable() {
print("==================================")
}
}
} }

View File

@@ -2,7 +2,7 @@
// Paths.swift // Paths.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@@ -12,63 +12,87 @@ import Foundation
The path to the Homebrew directory and the user's name are fetched only once, at boot. The path to the Homebrew directory and the user's name are fetched only once, at boot.
*/ */
public class Paths { public class Paths {
public static let shared = Paths() public static let shared = Paths()
private var baseDir : Paths.HomebrewDir internal var baseDir: Paths.HomebrewDir
private var userName : String private var userName: String
init() { init() {
baseDir = FileManager.default.fileExists(atPath: "\(HomebrewDir.opt.rawValue)/bin/brew") ? .opt : .usr baseDir = App.architecture != "x86_64" ? .opt : .usr
userName = String(Shell.pipe("whoami").split(separator: "\n")[0]) userName = String(Shell.pipe("whoami").split(separator: "\n")[0])
} }
public func detectBinaryPaths() {
detectComposerBinary()
}
// - MARK: Binaries // - MARK: Binaries
public static var valet: String { public static var valet: String {
return "\(binPath)/valet" return "\(binPath)/valet"
} }
public static var brew: String { public static var brew: String {
return "\(binPath)/brew" return "\(binPath)/brew"
} }
public static var php: String { public static var php: String {
return "\(binPath)/php" return "\(binPath)/php"
} }
public static var phpConfig: String { public static var phpConfig: String {
return "\(binPath)/php-config" return "\(binPath)/php-config"
} }
// - MARK: Detected Binaries
/** The path to the Composer binary. Can be in multiple locations, so is detected instead. */
public static var composer: String?
// - MARK: Paths // - MARK: Paths
public static var whoami: String { public static var whoami: String {
return shared.userName return shared.userName
} }
public static var cellarPath: String { public static var cellarPath: String {
return "\(shared.baseDir.rawValue)/Cellar" return "\(shared.baseDir.rawValue)/Cellar"
} }
public static var binPath: String { public static var binPath: String {
return "\(shared.baseDir.rawValue)/bin" return "\(shared.baseDir.rawValue)/bin"
} }
public static var optPath: String { public static var optPath: String {
return "\(shared.baseDir.rawValue)/opt" return "\(shared.baseDir.rawValue)/opt"
} }
public static var etcPath: String { public static var etcPath: String {
return "\(shared.baseDir.rawValue)/etc" return "\(shared.baseDir.rawValue)/etc"
} }
// MARK: - Flexible Binaries
// (these can be in multiple locations, so we scan common places because)
// (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() {
if Filesystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer"
} else if Filesystem.fileExists("/opt/homebrew/bin/composer") {
Paths.composer = "/opt/homebrew/bin/composer"
} else {
Paths.composer = nil
Log.warn("Composer was not found.")
}
}
// MARK: - Enum // MARK: - Enum
public enum HomebrewDir: String { public enum HomebrewDir: String {
case opt = "/opt/homebrew" case opt = "/opt/homebrew"
case usr = "/usr/local" case usr = "/usr/local"
} }
} }

View File

@@ -0,0 +1,62 @@
//
// Process.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
extension Process {
/**
When a process is running in the background, it can send content to standard
output or standard error, just like it would in a terminal. Using `listen`
allows us to react whenever data is received by running a particular closure,
depending on which type of data is received.
*/
public func listen(
didReceiveStandardOutputData: @escaping (String) -> Void,
didReceiveStandardErrorData: @escaping (String) -> Void
) {
let outputPipe = Pipe()
let errorPipe = Pipe()
self.standardOutput = outputPipe
self.standardError = errorPipe
[
(outputPipe, didReceiveStandardOutputData),
(errorPipe, didReceiveStandardErrorData)
].forEach { (pipe: Pipe, callback: @escaping (String) -> Void) in
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading,
queue: nil
) { _ in
if let outputString = String(
data: pipe.fileHandleForReading.availableData,
encoding: String.Encoding.utf8
) {
callback(outputString)
}
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}
}
/**
After the process is done running, you'll want to stop listening.
*/
public func haltListening() {
if let pipe = self.standardOutput as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
if let pipe = self.standardError as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
}
}

View File

@@ -2,41 +2,41 @@
// Shell.swift // Shell.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
public class Shell { public class Shell {
// MARK: - Invoke static functions // MARK: - Invoke static functions
public static func run( public static func run(
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) { ) {
Shell.user.run(command, requiresPath: requiresPath) Shell.user.run(command, requiresPath: requiresPath)
} }
public static func pipe( public static func pipe(
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) -> String { ) -> String {
return Shell.user.pipe(command, requiresPath: requiresPath) return Shell.user.pipe(command, requiresPath: requiresPath)
} }
// MARK: - Singleton // MARK: - Singleton
/** /**
We now require macOS 11, so no need to detect which terminal to use. We now require macOS 11, so no need to detect which terminal to use.
*/ */
public var shell: String = "/bin/sh" public var shell: String = "/bin/sh"
/** /**
Singleton to access a user shell (with --login) Singleton to access a user shell (with --login)
*/ */
public static let user = Shell() public static let user = Shell()
/** /**
Runs a shell command without using the output. Runs a shell command without using the output.
Uses the default shell. Uses the default shell.
@@ -51,7 +51,7 @@ public class Shell {
// Equivalent of piping to /dev/null; don't do anything with the string // Equivalent of piping to /dev/null; don't do anything with the string
_ = Shell.pipe(command, requiresPath: requiresPath) _ = Shell.pipe(command, requiresPath: requiresPath)
} }
/** /**
Runs a shell command and returns the output. Runs a shell command and returns the output.
@@ -69,7 +69,7 @@ public class Shell {
) )
return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput return !hasError ? shellOutput.standardOutput : shellOutput.errorOutput
} }
/** /**
Runs the command and returns a `ShellOutput` object, which contains info about the process. Runs the command and returns a `ShellOutput` object, which contains info about the process.
@@ -81,17 +81,17 @@ public class Shell {
_ command: String, _ command: String,
requiresPath: Bool = false requiresPath: Bool = false
) -> Shell.Output { ) -> Shell.Output {
let outputPipe = Pipe() let outputPipe = Pipe()
let errorPipe = Pipe() let errorPipe = Pipe()
let task = self.createTask(for: command, requiresPath: requiresPath) let task = self.createTask(for: command, requiresPath: requiresPath)
task.standardOutput = outputPipe task.standardOutput = outputPipe
task.standardError = errorPipe task.standardError = errorPipe
task.launch() task.launch()
task.waitUntilExit() task.waitUntilExit()
return Shell.Output( let output = Shell.Output(
standardOutput: String( standardOutput: String(
data: outputPipe.fileHandleForReading.readDataToEndOfFile(), data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
encoding: .utf8 encoding: .utf8
@@ -102,17 +102,14 @@ public class Shell {
)!, )!,
task: task task: task
) )
if CommandLine.arguments.contains("--v") {
log(task: task, output: output)
}
return output
} }
/**
Checks if a file exists at a certain path.
Used to be done with a shell command, now uses the native FileManager class instead.
*/
public static func fileExists(_ path: String) -> Bool {
let fullPath = path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
return FileManager.default.fileExists(atPath: fullPath)
}
/** /**
Creates a new process with the correct PATH and shell. Creates a new process with the correct PATH and shell.
*/ */
@@ -120,55 +117,36 @@ public class Shell {
let tailoredCommand = requiresPath let tailoredCommand = requiresPath
? "export PATH=\(Paths.binPath):$PATH && \(command)" ? "export PATH=\(Paths.binPath):$PATH && \(command)"
: command : command
let task = Process() let task = Process()
task.launchPath = self.shell task.launchPath = self.shell
task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand] task.arguments = ["--noprofile", "-norc", "--login", "-c", tailoredCommand]
return task return task
} }
public static func captureOutput( /**
_ task: Process, Verbose logging for PHP Monitor's synchronous shell output.
didReceiveStdOutData: @escaping (String) -> Void, */
didReceiveStdErrData: @escaping (String) -> Void private func log(task: Process, output: Output) {
) { Log.info("")
let outputPipe = Pipe() Log.info("==== COMMAND ====")
let errorPipe = Pipe() Log.info("")
Log.info("\(self.shell) \(task.arguments?.joined(separator: " ") ?? "")")
task.standardOutput = outputPipe Log.info("")
task.standardError = errorPipe Log.info("==== OUTPUT ====")
Log.info("")
[(outputPipe, didReceiveStdOutData), (errorPipe, didReceiveStdErrData)].forEach { dump(output)
(pipe: Pipe, callback: @escaping (String) -> Void) in Log.info("")
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify() Log.info("==== END OUTPUT ====")
NotificationCenter.default.addObserver( Log.info("")
forName: NSNotification.Name.NSFileHandleDataAvailable,
object: pipe.fileHandleForReading,
queue: nil
) { notification in
if let outputString = String(data: pipe.fileHandleForReading.availableData, encoding: String.Encoding.utf8) {
callback(outputString)
}
pipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
}
}
} }
public static func haltCapturingOutput(_ task: Process) {
if let pipe = task.standardOutput as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
if let pipe = task.standardError as? Pipe {
NotificationCenter.default.removeObserver(pipe.fileHandleForReading)
}
}
public class Output { public class Output {
public let standardOutput: String public let standardOutput: String
public let errorOutput: String public let errorOutput: String
public let task: Process public let task: Process
init(standardOutput: String, init(standardOutput: String,
errorOutput: String, errorOutput: String,
task: Process) { task: Process) {

View File

@@ -15,9 +15,9 @@ struct HomebrewPermissionError: Error, AlertableError {
enum Kind: String { enum Kind: String {
case applescriptNilError = "homebrew_permissions.applescript_returned_nil" case applescriptNilError = "homebrew_permissions.applescript_returned_nil"
} }
let kind: Kind let kind: Kind
func getErrorMessageKey() -> String { func getErrorMessageKey() -> String {
return "alert.errors.\(self.kind.rawValue)" return "alert.errors.\(self.kind.rawValue)"
} }

View File

@@ -2,17 +2,17 @@
// Date.swift // Date.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
extension Date { extension Date {
func toString() -> String { func toString() -> String {
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return dateFormatter.string(from: self) return dateFormatter.string(from: self)
} }
} }

View File

@@ -3,27 +3,27 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 14/04/2021. // Created by Nico Verbruggen on 14/04/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
extension NSMenu { extension NSMenu {
open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) { open func addItem(_ newItem: NSMenuItem, withKeyModifier modifier: NSEvent.ModifierFlags) {
newItem.keyEquivalentModifierMask = modifier newItem.keyEquivalentModifierMask = modifier
self.addItem(newItem) self.addItem(newItem)
} }
} }
@IBDesignable class LocalizedMenuItem: NSMenuItem { @IBDesignable class LocalizedMenuItem: NSMenuItem {
@IBInspectable @IBInspectable
var localizationKey: String? { var localizationKey: String? {
didSet { didSet {
self.title = localizationKey?.localized ?? self.title self.title = localizationKey?.localized ?? self.title
} }
} }
} }

View File

@@ -0,0 +1,38 @@
//
// NSWindowExtension.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/02/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
extension NSWindow {
/**
Shakes a window. Inspired by: http://blog.ericd.net/2016/09/30/shaking-a-macos-window/
*/
func shake() {
let numberOfShakes = 3, durationOfShake = 0.2, vigourOfShake: CGFloat = 0.03
let frame: CGRect = self.frame
let shakeAnimation: CAKeyframeAnimation = CAKeyframeAnimation()
let shakePath = CGMutablePath()
shakePath.move( to: CGPoint(x: frame.minX, y: frame.minY))
for _ in 0...numberOfShakes-1 {
shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
}
shakePath.closeSubpath()
shakeAnimation.path = shakePath
shakeAnimation.duration = durationOfShake
self.animations = ["frameOrigin": shakeAnimation]
self.animator().setFrameOrigin(self.frame.origin)
}
}

View File

@@ -2,33 +2,33 @@
// StringExtension.swift // StringExtension.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
extension String { extension String {
var localized: String { var localized: String {
return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "") return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
} }
func localized(_ args: CVarArg...) -> String { func localized(_ args: CVarArg...) -> String {
String(format: self.localized, arguments: args) String(format: self.localized, arguments: args)
} }
func countInstances(of stringToFind: String) -> Int { func countInstances(of stringToFind: String) -> Int {
if (stringToFind.isEmpty) { if stringToFind.isEmpty {
return 0 return 0
} }
var count = 0 var count = 0
var searchRange: Range<String.Index>? var searchRange: Range<String.Index>?
while let foundRange = range(of: stringToFind, options: [], range: searchRange) { while let foundRange = range(of: stringToFind, options: [], range: searchRange) {
count += 1 count += 1
searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex)) searchRange = Range(uncheckedBounds: (lower: foundRange.upperBound, upper: endIndex))
} }
return count return count
} }
@@ -37,7 +37,7 @@ extension String {
let end = r.upperBound let end = r.upperBound
return String(self[start ..< end]) return String(self[start ..< end])
} }
// Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/ // Code taken from: https://sarunw.com/posts/how-to-compare-two-app-version-strings-in-swift/
/* /*
<1> We split the version by period (.). <1> We split the version by period (.).
@@ -50,12 +50,12 @@ extension String {
*/ */
func versionCompare(_ otherVersion: String) -> ComparisonResult { func versionCompare(_ otherVersion: String) -> ComparisonResult {
let versionDelimiter = "." let versionDelimiter = "."
var versionComponents = self.components(separatedBy: versionDelimiter) // <1> var versionComponents = self.components(separatedBy: versionDelimiter) // <1>
var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter) var otherVersionComponents = otherVersion.components(separatedBy: versionDelimiter)
let zeroDiff = versionComponents.count - otherVersionComponents.count // <2> let zeroDiff = versionComponents.count - otherVersionComponents.count // <2>
if zeroDiff == 0 { // <3> if zeroDiff == 0 { // <3>
// Same format, compare normally // Same format, compare normally
return self.compare(otherVersion, options: .numeric) return self.compare(otherVersion, options: .numeric)
@@ -70,5 +70,5 @@ extension String {
.compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6> .compare(otherVersionComponents.joined(separator: versionDelimiter), options: .numeric) // <6>
} }
} }
} }

View File

@@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 04/02/2021. // Created by Nico Verbruggen on 04/02/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@@ -12,25 +12,25 @@ import Cocoa
// Adapted from: https://stackoverflow.com/a/46268778 // Adapted from: https://stackoverflow.com/a/46268778
protocol XibLoadable { protocol XibLoadable {
static var xibName: String? { get } static var xibName: String? { get }
static func createFromXib(in bundle: Bundle) -> Self? static func createFromXib(in bundle: Bundle) -> Self?
} }
extension XibLoadable where Self: NSView { extension XibLoadable where Self: NSView {
static var xibName: String? { static var xibName: String? {
return String(describing: Self.self) return String(describing: Self.self)
} }
static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? { static func createFromXib(in bundle: Bundle = Bundle.main) -> Self? {
guard let xibName = xibName else { return nil } guard let xibName = xibName else { return nil }
var topLevelArray: NSArray? = nil var topLevelArray: NSArray?
bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray) bundle.loadNibNamed(NSNib.Name(xibName), owner: self, topLevelObjects: &topLevelArray)
guard let results = topLevelArray else { return nil } guard let results = topLevelArray else { return nil }
let views = Array<Any>(results).filter { $0 is Self } let views = [Any](results).filter { $0 is Self }
return views.last as? Self return views.last as? Self
} }
} }

View File

@@ -2,31 +2,13 @@
// Alert.swift // Alert.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class Alert { class Alert {
public static func present(
messageText: String,
informativeText: String,
buttonTitle: String = "OK",
secondButtonTitle: String = "",
style: NSAlert.Style = .informational
) -> Bool {
let alert = NSAlert.init()
alert.alertStyle = style
alert.messageText = messageText
alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) {
alert.addButton(withTitle: secondButtonTitle)
}
return alert.runModal() == .alertFirstButtonReturn
}
public static func confirm( public static func confirm(
onWindow window: NSWindow, onWindow window: NSWindow,
messageText: String, messageText: String,
@@ -36,12 +18,16 @@ class Alert {
style: NSAlert.Style = .warning, style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void) onFirstButtonPressed: @escaping (() -> Void)
) { ) {
if !Thread.isMainThread {
fatalError("You should always present alerts on the main thread!")
}
let alert = NSAlert.init() let alert = NSAlert.init()
alert.alertStyle = style alert.alertStyle = style
alert.messageText = messageText alert.messageText = messageText
alert.informativeText = informativeText alert.informativeText = informativeText
alert.addButton(withTitle: buttonTitle) alert.addButton(withTitle: buttonTitle)
if (!secondButtonTitle.isEmpty) { if !secondButtonTitle.isEmpty {
alert.addButton(withTitle: secondButtonTitle) alert.addButton(withTitle: secondButtonTitle)
} }
alert.beginSheetModal(for: window) { response in alert.beginSheetModal(for: window) { response in
@@ -50,32 +36,5 @@ class Alert {
} }
} }
} }
/**
Notify the user about something by showing an alert.
*/
public static func notify(message: String, info: String, style: NSAlert.Style = .informational) {
_ = present(
messageText: message,
informativeText: info,
buttonTitle: "OK",
secondButtonTitle: "",
style: style
)
}
/**
Notify the user about a particular error (which must be `Alertable`)
by showing an alert.
*/
public static func notify(about error: Error & AlertableError) {
let key = error.getErrorMessageKey()
_ = present(
messageText: "\(key).title".localized,
informativeText: "\(key).description".localized,
buttonTitle: "OK",
secondButtonTitle: "",
style: .critical
)
}
} }

View File

@@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 07/12/2021. // Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@@ -12,23 +12,23 @@ import Foundation
/// In most cases this is going to be a code editor, but it could also be another application /// In most cases this is going to be a code editor, but it could also be another application
/// that supports opening those directories, like a visual Git client or a terminal app. /// that supports opening those directories, like a visual Git client or a terminal app.
class Application { class Application {
enum AppType { enum AppType {
case editor, browser, git_gui, terminal, user_supplied case editor, browser, git_gui, terminal, user_supplied
} }
/// Name of the app. Used for display purposes and to determine `name.app` exists. /// Name of the app. Used for display purposes and to determine `name.app` exists.
let name: String let name: String
/// Application type. Depending on the type, a different action might occur. /// Application type. Depending on the type, a different action might occur.
let type: AppType let type: AppType
/// Initializer. Used to detect a specific app of a specific type. /// Initializer. Used to detect a specific app of a specific type.
init(_ name: String, _ type: AppType) { init(_ name: String, _ type: AppType) {
self.name = name self.name = name
self.type = type self.type = type
} }
/** /**
Attempt to open a specific directory in the app of choice. Attempt to open a specific directory in the app of choice.
(This will open the app if it isn't open yet.) (This will open the app if it isn't open yet.)
@@ -36,7 +36,7 @@ class Application {
@objc public func openDirectory(file: String) { @objc public func openDirectory(file: String) {
return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"") return Shell.run("/usr/bin/open -a \"\(name)\" \"\(file)\"")
} }
/** Checks if the app is installed. */ /** Checks if the app is installed. */
func isInstalled() -> Bool { func isInstalled() -> Bool {
// If this script does not complain, the app exists! // If this script does not complain, the app exists!
@@ -45,7 +45,7 @@ class Application {
requiresPath: false requiresPath: false
).task.terminationStatus == 0 ).task.terminationStatus == 0
} }
/** /**
Detect which apps are available to open a specific directory. Detect which apps are available to open a specific directory.
*/ */

View File

@@ -1,29 +0,0 @@
//
// Async.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/01/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
This generic async helper is something I'd like to use in more places.
The `DispatchQueue.global` into `DispatchQueue.main.async` logic is common in the app.
I could also use try `async` support which was introduced in Swift but that would
require too much refactoring at this time to consider. I also need to read up on async
in order to properly grasp all the gotchas. Looking into that later at some point.
*/
public func runAsync(_ execute: @escaping () -> Void, completion: @escaping () -> Void = {})
{
DispatchQueue.global(qos: .userInitiated).async {
execute()
DispatchQueue.main.async {
completion()
}
}
}

View File

@@ -3,13 +3,13 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 07/12/2021. // Created by Nico Verbruggen on 07/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class Filesystem { class Filesystem {
/** /**
Checks if a file exists at the provided path. Checks if a file exists at the provided path.
Uses `FileManager`. Uses `FileManager`.
@@ -19,5 +19,5 @@ class Filesystem {
atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)") atPath: path.replacingOccurrences(of: "~", with: "/Users/\(Paths.whoami)")
) )
} }
} }

View File

@@ -2,26 +2,26 @@
// LocalNotification.swift // LocalNotification.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
import UserNotifications import UserNotifications
class LocalNotification { class LocalNotification {
public static func send(title: String, subtitle: String) { public static func send(title: String, subtitle: String) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = title content.title = title
content.body = subtitle content.body = subtitle
let uuidString = UUID().uuidString let uuidString = UUID().uuidString
let request = UNNotificationRequest( let request = UNNotificationRequest(
identifier: uuidString, identifier: uuidString,
content: content, content: content,
trigger: nil trigger: nil
) )
let notificationCenter = UNUserNotificationCenter.current() let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.add(request) { (error) in notificationCenter.add(request) { (error) in
if error != nil { if error != nil {
@@ -29,5 +29,5 @@ class LocalNotification {
} }
} }
} }
} }

View File

@@ -2,46 +2,46 @@
// ImageGenerator.swift // ImageGenerator.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
class MenuBarImageGenerator { class MenuBarImageGenerator {
/** /**
Takes a string and converts it to an image that can be displayed in the menu bar. Takes a string and converts it to an image that can be displayed in the menu bar.
The width of the NSImage depends on the length of the text. The width of the NSImage depends on the length of the text.
*/ */
public static func textToImage(text: String) -> NSImage { public static func textToImage(text: String) -> NSImage {
let font = NSFont.systemFont(ofSize: 14, weight: .medium) let font = NSFont.systemFont(ofSize: 14, weight: .medium)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [ let textFontAttributes = [
NSAttributedString.Key.font: font, NSAttributedString.Key.font: font,
NSAttributedString.Key.foregroundColor: NSColor.black, NSAttributedString.Key.foregroundColor: NSColor.black,
NSAttributedString.Key.paragraphStyle: textStyle NSAttributedString.Key.paragraphStyle: textStyle
] ]
let padding : CGFloat = 2.0; let padding: CGFloat = 2.0
// Create an attributed string so we'll know how wide the item will need to be // Create an attributed string so we'll know how wide the item will need to be
let attributedString = NSAttributedString(string: text, attributes: textFontAttributes) let attributedString = NSAttributedString(string: text, attributes: textFontAttributes)
let textSize = attributedString.size() let textSize = attributedString.size()
// Add padding to the width of the menu bar item // Add padding to the width of the menu bar item
let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height) let size = NSSize(width: textSize.width + (2 * padding), height: textSize.height)
let image = NSImage(size: size) let image = NSImage(size: size)
// Set the image rect with the appropriate dimensions // Set the image rect with the appropriate dimensions
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
// Position the text inside the image rect // Position the text inside the image rect
let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height) let textRect = CGRect(x: padding, y: 0.5, width: image.size.width, height: image.size.height)
let targetImage: NSImage = NSImage(size: image.size) let targetImage: NSImage = NSImage(size: image.size)
let representation: NSBitmapImageRep = NSBitmapImageRep( let representation: NSBitmapImageRep = NSBitmapImageRep(
bitmapDataPlanes: nil, bitmapDataPlanes: nil,
pixelsWide: Int(image.size.width), pixelsWide: Int(image.size.width),
@@ -54,40 +54,40 @@ class MenuBarImageGenerator {
bytesPerRow: 0, bytesPerRow: 0,
bitsPerPixel: 0 bitsPerPixel: 0
)! )!
targetImage.addRepresentation(representation) targetImage.addRepresentation(representation)
targetImage.lockFocus() targetImage.lockFocus()
image.draw(in: imageRect) image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes) text.draw(in: textRect, withAttributes: textFontAttributes)
targetImage.unlockFocus() targetImage.unlockFocus()
return targetImage return targetImage
} }
/** /**
The same as before, but also attempts to add an icon to the left. The same as before, but also attempts to add an icon to the left.
*/ */
public static func textToImageWithIcon(text: String) -> NSImage { public static func textToImageWithIcon(text: String) -> NSImage {
// We'll start out with the image containing the text // We'll start out with the image containing the text
let textImage = self.textToImage(text: text) let textImage = self.textToImage(text: text)
// Then we'll fetch the image we want on the left // Then we'll fetch the image we want on the left
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
if iconType == nil { if iconType == nil {
Log.warn("Invalid icon type found, using the default") Log.warn("Invalid icon type found, using the default")
iconType = MenuBarIcon.iconPhp.rawValue iconType = MenuBarIcon.iconPhp.rawValue
} }
let iconImage = NSImage(named: "MenuBar_\(iconType!)")! let iconImage = NSImage(named: "MenuBar_\(iconType!)")!
// We'll need to reference the width of the icon a bunch of times // We'll need to reference the width of the icon a bunch of times
let iconWidthSize = iconImage.size.width let iconWidthSize = iconImage.size.width
// There will also be an additional divider between the image and the text (image) // There will also be an additional divider between the image and the text (image)
let divider: CGFloat = 3 let divider: CGFloat = 3
// Use a fixed size for the height of the menu bar (18pt) // Use a fixed size for the height of the menu bar (18pt)
let imageRect = CGRect( let imageRect = CGRect(
x: 0, x: 0,
@@ -95,14 +95,14 @@ class MenuBarImageGenerator {
width: textImage.size.width + iconWidthSize + divider, width: textImage.size.width + iconWidthSize + divider,
height: 18 height: 18
) )
// Create a new image, we'll draw the text and our icon in there // Create a new image, we'll draw the text and our icon in there
let image: NSImage = NSImage(size: imageRect.size) let image: NSImage = NSImage(size: imageRect.size)
image.lockFocus() image.lockFocus()
// Calculate the offset between the image and the text // Calculate the offset between the image and the text
let offset = imageRect.size.width - textImage.size.width let offset = imageRect.size.width - textImage.size.width
// Draw the text with a negative x offset (so there is room on the left for the icon) // Draw the text with a negative x offset (so there is room on the left for the icon)
textImage.draw( textImage.draw(
in: imageRect, in: imageRect,
@@ -115,7 +115,7 @@ class MenuBarImageGenerator {
operation: .overlay, operation: .overlay,
fraction: 1 fraction: 1
) )
// Draw the icon directly in the left of the imageRect (where we left space) // Draw the icon directly in the left of the imageRect (where we left space)
iconImage.draw( iconImage.draw(
in: imageRect, in: imageRect,
@@ -128,11 +128,11 @@ class MenuBarImageGenerator {
operation: .overlay, operation: .overlay,
fraction: 1 fraction: 1
) )
// We're done with this image // We're done with this image
image.unlockFocus() image.unlockFocus()
return image return image
} }
} }

View File

@@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 05/12/2021. // Created by Nico Verbruggen on 05/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Cocoa import Cocoa
@@ -15,32 +15,32 @@ import Cocoa
- Note: This class does make a simple assumption: each window controller corresponds to a single view. - Note: This class does make a simple assumption: each window controller corresponds to a single view.
*/ */
class PMWindowController: NSWindowController, NSWindowDelegate { class PMWindowController: NSWindowController, NSWindowDelegate {
public var windowName: String { public var windowName: String {
fatalError("Please specify a window name") fatalError("Please specify a window name")
} }
override func showWindow(_ sender: Any?) { override func showWindow(_ sender: Any?) {
super.showWindow(sender) super.showWindow(sender)
App.shared.register(window: windowName) App.shared.register(window: windowName)
} }
func windowWillClose(_ notification: Notification) { func windowWillClose(_ notification: Notification) {
App.shared.remove(window: windowName) App.shared.remove(window: windowName)
} }
deinit { deinit {
Log.perf("Window controller '\(windowName)' was deinitialized") Log.perf("Window controller '\(windowName)' was deinitialized")
} }
} }
extension NSWindowController { extension NSWindowController {
public func positionWindowInTopLeftCorner() { public func positionWindowInTopLeftCorner() {
guard let frame = NSScreen.main?.frame else { return } guard let frame = NSScreen.main?.frame else { return }
guard let window = self.window else { return } guard let window = self.window else { return }
window.setFrame(NSRect( window.setFrame(NSRect(
x: frame.size.width - window.frame.size.width - 20, x: frame.size.width - window.frame.size.width - 20,
y: frame.size.height - window.frame.size.height - 40, y: frame.size.height - window.frame.size.height - 40,
@@ -48,5 +48,5 @@ extension NSWindowController {
height: window.frame.height height: window.frame.height
), display: true) ), display: true)
} }
} }

View File

@@ -3,15 +3,15 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 16/12/2021. // Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class VersionExtractor { class VersionExtractor {
/** /**
This attempts to extract the version number from the command line output of Valet. This attempts to extract the version number from any given string.
*/ */
public static func from(_ string: String) -> String? { public static func from(_ string: String) -> String? {
do { do {
@@ -19,26 +19,26 @@ class VersionExtractor {
pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#, pattern: #"(?<version>(\d+)(.)(\d+)((.)(\d+))?)"#,
options: [] options: []
) )
let match = regex.matches( let match = regex.matches(
in: string, in: string,
options: [], options: [],
range: NSMakeRange(0, string.count) range: NSRange(location: 0, length: string.count)
).first ).first
guard let match = match else { guard let match = match else {
return nil return nil
} }
let range = Range( let range = Range(
match.range(withName: "version"), match.range(withName: "version"),
in: string in: string
)! )!
return String(string[range]) return String(string[range])
} catch { } catch {
return nil return nil
} }
} }
} }

View File

@@ -2,7 +2,7 @@
// ActivePhpInstallation.swift // ActivePhpInstallation.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@@ -21,78 +21,78 @@ class ActivePhpInstallation {
var version: Version! var version: Version!
var limits: Limits! var limits: Limits!
var extensions: [PhpExtension]! var extensions: [PhpExtension]!
// MARK: - Computed // MARK: - Computed
var formula: String { var formula: String {
return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)" return (version.short == PhpEnv.brewPhpVersion) ? "php" : "php@\(version.short)"
} }
// MARK: - Initializer // MARK: - Initializer
init() { init() {
// Show information about the current version // Show information about the current version
getVersion() getVersion()
// If an error occurred, exit early // If an error occurred, exit early
if (version.error) { if version.error {
limits = Limits() limits = Limits()
extensions = [] extensions = []
return return
} }
// Load extension information // Load extension information
let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
extensions = PhpExtension.load(from: path) extensions = PhpExtension.load(from: path)
// Get configuration values // Get configuration values
limits = Limits( limits = Limits(
memory_limit: getByteCount(key: "memory_limit"), memory_limit: getByteCount(key: "memory_limit"),
upload_max_filesize: getByteCount(key: "upload_max_filesize"), upload_max_filesize: getByteCount(key: "upload_max_filesize"),
post_max_size: getByteCount(key: "post_max_size") post_max_size: getByteCount(key: "post_max_size")
) )
// Return a list of .ini files parsed after php.ini // Return a list of .ini files parsed after php.ini
let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"]) let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"])
.replacingOccurrences(of: "\n", with: "") .replacingOccurrences(of: "\n", with: "")
.split(separator: ",") .split(separator: ",")
.map { String($0) } .map { String($0) }
// See if any extensions are present in said .ini files // See if any extensions are present in said .ini files
paths.forEach { (iniFilePath) in paths.forEach { (iniFilePath) in
let exts = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath)) let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath))
if exts.count > 0 { if !loadedExtensions.isEmpty {
extensions.append(contentsOf: exts) extensions.append(contentsOf: loadedExtensions)
} }
} }
} }
/** /**
When the app tries to retrieve the version, the installation is considered broken if the output is nothing, 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. _or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case.
*/ */
private func getVersion() -> Void { private func getVersion() {
self.version = Version() self.version = Version()
let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true) let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true)
if (version == "" || version.contains("Warning") || version.contains("Error")) { if version == "" || version.contains("Warning") || version.contains("Error") {
self.version.short = "💩 BROKEN" self.version.short = "💩 BROKEN"
self.version.long = "" self.version.long = ""
self.version.error = true self.version.error = true
return return
} }
// That's the long version // That's the long version
self.version.long = version self.version.long = version
// Next up, let's strip away the minor version number // Next up, let's strip away the minor version number
let segments = self.version.long.components(separatedBy: ".") let segments = self.version.long.components(separatedBy: ".")
// Get the first two elements // Get the first two elements
self.version.short = segments[0...1].joined(separator: ".") self.version.short = segments[0...1].joined(separator: ".")
} }
/** /**
Retrieves the display value for a specific key in the `.ini` file. Retrieves the display value for a specific key in the `.ini` file.
@@ -110,18 +110,18 @@ class ActivePhpInstallation {
*/ */
private func getByteCount(key: String) -> String { 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)');"])
// Check if the value is unlimited // Check if the value is unlimited
if (value == "-1") { if value == "-1" {
return "" return ""
} }
// Check if the syntax is valid otherwise // Check if the syntax is valid otherwise
let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: [])
let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first let match = regex.matches(in: value, options: [], range: NSRange(location: 0, length: value.count)).first
return (match == nil) ? "⚠️" : "\(value)B" return (match == nil) ? "⚠️" : "\(value)B"
} }
/** /**
Determine if PHP-FPM is configured correctly. Determine if PHP-FPM is configured correctly.
@@ -135,11 +135,11 @@ class ActivePhpInstallation {
let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf"
return Shell.pipe("cat \(fileName)").contains("valet.sock") return Shell.pipe("cat \(fileName)").contains("valet.sock")
} }
// Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :)
return Shell.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 // MARK: - Structs
/** /**
@@ -153,7 +153,7 @@ class ActivePhpInstallation {
var long = "???" var long = "???"
var error = false var error = false
} }
/** /**
Struct containing information about the limits of the current PHP installation. Struct containing information about the limits of the current PHP installation.
Includes: memory limit, max upload size and max post size. Includes: memory limit, max upload size and max post size.
@@ -163,5 +163,5 @@ class ActivePhpInstallation {
var upload_max_filesize = "???" var upload_max_filesize = "???"
var post_max_size = "???" var post_max_size = "???"
} }
} }

View File

@@ -0,0 +1,33 @@
//
// Xdebug.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/05/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class Xdebug {
public static var enabled: Bool {
return !self.mode.isEmpty
}
public static var mode: String {
return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"])
}
public static var modes: [String] {
return [
"off",
"develop",
"coverage",
"debug",
"gcstats",
"profile",
"trace"
]
}
}

View File

@@ -2,24 +2,24 @@
// HomebrewPackage.swift // HomebrewPackage.swift
// PHP Monitor // PHP Monitor
// //
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
struct HomebrewPackage: Decodable { struct HomebrewPackage: Decodable {
let name: String let name: String
let full_name: String let full_name: String
let aliases: [String] let aliases: [String]
let installed: [HomebrewInstalled] let installed: [HomebrewInstalled]
let linked_keg: String? let linked_keg: String?
public var version: String { public var version: String {
return aliases.first! return aliases.first!
.replacingOccurrences(of: "php@", with: "") .replacingOccurrences(of: "php@", with: "")
} }
} }
struct HomebrewInstalled: Decodable { struct HomebrewInstalled: Decodable {

View File

@@ -18,4 +18,21 @@ struct HomebrewService: Decodable, Equatable {
let status: String? let status: String?
let log_path: String? let log_path: String?
let error_log_path: String? let error_log_path: String?
public static func loadAll(
filter: [String] = [PhpEnv.phpInstall.formula, "nginx", "dnsmasq"],
completion: @escaping ([HomebrewService]) -> Void
) {
DispatchQueue.global(qos: .background).async {
let data = Shell
.pipe("sudo \(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)!
let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data)
.filter({ return filter.contains($0.name) })
completion(services)
}
}
} }

View File

@@ -3,48 +3,48 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 21/12/2021. // Created by Nico Verbruggen on 21/12/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class PhpEnv { class PhpEnv {
// MARK: - Initializer // MARK: - Initializer
init() { init() {
self.currentInstall = ActivePhpInstallation() self.currentInstall = ActivePhpInstallation()
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json"); let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
self.homebrewPackage = try! JSONDecoder().decode( self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self, [HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)! from: brewPhpAlias.data(using: .utf8)!
).first! ).first!
Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!") Log.info("When on your system, the `php` formula means version \(homebrewPackage.version)!")
} }
// MARK: - Properties // MARK: - Properties
/** The delegate that is informed of updates. */ /** The delegate that is informed of updates. */
weak var delegate: PhpSwitcherDelegate? weak var delegate: PhpSwitcherDelegate?
/** The static app instance. Accessible at any time. */ /** The static app instance. Accessible at any time. */
static let shared = PhpEnv() static let shared = PhpEnv()
/** Whether the switcher is busy performing any actions. */ /** Whether the switcher is busy performing any actions. */
var isBusy: Bool = false var isBusy: Bool = false
/** All available versions of PHP. */ /** All available versions of PHP. */
var availablePhpVersions: [String] = [] var availablePhpVersions: [String] = []
/** Cached information about the PHP installations. */ /** Cached information about the PHP installations. */
var cachedPhpInstallations: [String: PhpInstallation] = [:] var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */ /** 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. The version that the `php` formula via Brew is aliased to on the current system.
@@ -57,73 +57,82 @@ class PhpEnv {
static var brewPhpVersion: String { static var brewPhpVersion: String {
return Self.shared.homebrewPackage.version return Self.shared.homebrewPackage.version
} }
/** /**
The currently linked and active PHP installation. The currently linked and active PHP installation.
*/ */
static var phpInstall: ActivePhpInstallation { static var phpInstall: ActivePhpInstallation {
return Self.shared.currentInstall return Self.shared.currentInstall
} }
/** /**
Information we were able to discern from the Homebrew info command. Information we were able to discern from the Homebrew info command.
*/ */
var homebrewPackage: HomebrewPackage! = nil var homebrewPackage: HomebrewPackage! = nil
// MARK: - Methods // MARK: - Methods
public static var switcher: PhpSwitcher { public static var switcher: PhpSwitcher {
return InternalSwitcher() return InternalSwitcher()
} }
public static func detectPhpVersions() -> Void { public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions() _ = Self.shared.detectPhpVersions()
} }
/** /**
Detects which versions of PHP are installed. Detects which versions of PHP are installed.
*/ */
public func detectPhpVersions() -> [String] public func detectPhpVersions() -> [String] {
{
let files = Shell.pipe("ls \(Paths.optPath) | grep php@") let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n")) var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected // Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0` // The user may have `php` installed, but not e.g. `php@8.0`
// We should also detect that as a version that is installed // We should also detect that as a version that is installed
let phpAlias = homebrewPackage.version let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate // Avoid inserting a duplicate
if (!versionsOnly.contains(phpAlias) && Shell.fileExists("\(Paths.optPath)/php/bin/php")) { if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
versionsOnly.append(phpAlias) versionsOnly.append(phpAlias)
} }
Log.info("The PHP versions that were detected are: \(versionsOnly)") Log.info("The PHP versions that were detected are: \(versionsOnly)")
availablePhpVersions = versionsOnly availablePhpVersions = versionsOnly
var mappedVersions: [String: PhpInstallation] = [:] var mappedVersions: [String: PhpInstallation] = [:]
availablePhpVersions.forEach { version in availablePhpVersions.forEach { version in
mappedVersions[version] = PhpInstallation(version) mappedVersions[version] = PhpInstallation(version)
} }
cachedPhpInstallations = mappedVersions cachedPhpInstallations = mappedVersions
return versionsOnly return versionsOnly
} }
/** /**
Extracts valid PHP versions from an array of strings. Extracts valid PHP versions from an array of strings.
This array of strings is usually retrieved from `grep`. This array of strings is usually retrieved from `grep`.
If `generateHelpers` is set to true, after detecting
all versions, helper scripts are generated as well.
*/ */
public func extractPhpVersions( public func extractPhpVersions(
from versions: [String], from versions: [String],
checkBinaries: Bool = true checkBinaries: Bool = true,
generateHelpers: Bool = true
) -> [String] { ) -> [String] {
var output : [String] = [] var output: [String] = []
var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" }
}
versions.filter { (version) -> Bool in versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@ // Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected) // (e.g. something-php@8.0 won't be detected)
@@ -133,16 +142,19 @@ class PhpEnv {
// Only append the version if it doesn't already exist (avoid dupes), // Only append the version if it doesn't already exist (avoid dupes),
// is supported and where the binary exists (avoids broken installs) // is supported and where the binary exists (avoids broken installs)
if !output.contains(version) if !output.contains(version)
&& Constants.SupportedPhpVersions.contains(version) && supported.contains(version)
&& (checkBinaries ? Shell.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) && (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
{
output.append(version) output.append(version)
} }
} }
if generateHelpers {
output.forEach { PhpHelper.generate(for: $0) }
}
return output return output
} }
public func validVersions(for constraint: String) -> [PhpVersionNumber] { public func validVersions(for constraint: String) -> [PhpVersionNumber] {
constraint.split(separator: "|").flatMap { constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection return PhpVersionNumberCollection
@@ -150,7 +162,7 @@ class PhpEnv {
.matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines)) .matching(constraint: $0.trimmingCharacters(in: .whitespacesAndNewlines))
} }
} }
/** /**
Validates whether the currently running version matches the provided version. Validates whether the currently running version matches the provided version.
*/ */
@@ -159,7 +171,7 @@ class PhpEnv {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.") Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
return true return true
} }
return false return false
} }
} }

View File

@@ -0,0 +1,61 @@
//
// PhpHelper.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 17/03/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class PhpHelper {
static let keyPhrase = "This file was automatically generated by PHP Monitor."
public static func generate(for version: String) {
// Take the PHP version (e.g. "7.2") and generate a dotless version
let dotless = version.replacingOccurrences(of: ".", with: "")
do {
let destination = "/usr/local/bin/pm\(dotless)"
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)")
} catch {
print(error)
Log.err("Could not write PHP Monitor helper for PHP \(version) to /usr/local/bin/pm\(dotless)")
}
}
}

View File

@@ -10,21 +10,21 @@ import Foundation
public struct PhpVersionNumberCollection: Equatable { public struct PhpVersionNumberCollection: Equatable {
let versions: [PhpVersionNumber] let versions: [PhpVersionNumber]
public static func make(from versions: [String]) -> Self { public static func make(from versions: [String]) -> Self {
return PhpVersionNumberCollection( return PhpVersionNumberCollection(
versions: versions.map { PhpVersionNumber.make(from: $0)! } versions: versions.map { try! PhpVersionNumber.parse($0) }
) )
} }
public var first: PhpVersionNumber? { public var first: PhpVersionNumber? {
return self.versions.first return self.versions.first
} }
public var all: [PhpVersionNumber] { public var all: [PhpVersionNumber] {
return self.versions return self.versions
} }
/** /**
Checks if any versions of PHP are valid for the constraint provided. Checks if any versions of PHP are valid for the constraint provided.
Due to the complexity of evaluating these, a important test is maintained. Due to the complexity of evaluating these, a important test is maintained.
@@ -61,13 +61,13 @@ public struct PhpVersionNumberCollection: Equatable {
// Strict constraint (e.g. "7.0") -> returns specific version // Strict constraint (e.g. "7.0") -> returns specific version
return self.versions.filter { $0.isSameAs(version, strict) } return self.versions.filter { $0.isSameAs(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .caretVersionRange) { 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 // 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 // ^7.2 will be compatible with all versions between 7.2 and 8.0
return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) } return self.versions.filter { $0.hasNewerMinorVersionOrPatch(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) { if let version = PhpVersionNumber.make(from: constraint, type: .tildeVersionRange) {
// Tilde range means that most specific digit is used as the basis. // Tilde range means that most specific digit is used as the basis.
return self.versions.filter { return self.versions.filter {
@@ -78,11 +78,11 @@ public struct PhpVersionNumberCollection: Equatable {
: $0.hasSameMajorButNewerOrSameMinor(version, strict) : $0.hasSameMajorButNewerOrSameMinor(version, strict)
} }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) { if let version = PhpVersionNumber.make(from: constraint, type: .greaterThanOrEqual) {
return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) } return self.versions.filter { $0.isSameAs(version, strict) || $0.isNewerThan(version, strict) }
} }
if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) { if let version = PhpVersionNumber.make(from: constraint, type: .greaterThan) {
return self.versions.filter { $0.isNewerThan(version, strict) } return self.versions.filter { $0.isNewerThan(version, strict) }
} }
@@ -95,41 +95,52 @@ public struct PhpVersionNumber: Equatable {
let major: Int let major: Int
let minor: Int let minor: Int
let patch: 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 { public func patch(_ strictFallback: Bool = true, _ constraint: PhpVersionNumber? = nil) -> Int {
return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999) return patch ?? (strictFallback ? 0 : constraint?.patch ?? 999)
} }
public var homebrewVersion: String { public var homebrewVersion: String {
return "\(major).\(minor)" return "\(major).\(minor)"
} }
public enum MatchType: String { public enum MatchType: String {
case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"# case versionOnly = #"^(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case caretVersionRange = #"^\^(?<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 tildeVersionRange = #"^~(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
case greaterThanOrEqual = #"^>=(?<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 greaterThan = #"^>(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
// TODO: (5.1) Handle these cases (even though I suspect these are uncommon) // TODO: (6.0) Handle these cases (even though I suspect these are uncommon)
/* /*
case smallerThanOrEqual = #"^<=(?<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"# case smallerThan = #"^<(?<major>\d+).(?<minor>\d+).?(?<patch>\d+)?\z"#
*/ */
} }
public static func parse(_ text: String) throws -> Self { public static func parse(_ text: String) throws -> Self {
guard let versionText = VersionExtractor.from(text) else { guard let versionText = VersionExtractor.from(text) else {
throw VersionParseError() throw VersionParseError()
} }
return Self.make(from: versionText)! return Self.make(from: versionText)!
} }
public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? { public static func make(from versionString: String, type: MatchType = .versionOnly) -> Self? {
let regex = try! NSRegularExpression(pattern: type.rawValue, options: []) let regex = try! NSRegularExpression(pattern: type.rawValue, options: [])
let match = regex.matches(in: versionString, options: [], range: NSMakeRange(0, versionString.count)).first
let match = regex.matches(
in: versionString,
options: [],
range: NSRange(location: 0, length: versionString.count)
).first
if match != nil { if match != nil {
let major = Int( let major = Int(
versionString[Range(match!.range(withName: "major"), in: versionString)!] versionString[Range(match!.range(withName: "major"), in: versionString)!]
@@ -137,24 +148,24 @@ public struct PhpVersionNumber: Equatable {
let minor = Int( let minor = Int(
versionString[Range(match!.range(withName: "minor"), in: versionString)!] versionString[Range(match!.range(withName: "minor"), in: versionString)!]
)! )!
var patch: Int? = nil var patch: Int?
if let minorRange = Range(match!.range(withName: "patch"), in: versionString) { if let minorRange = Range(match!.range(withName: "patch"), in: versionString) {
patch = Int(versionString[minorRange]) patch = Int(versionString[minorRange])
} }
return Self(major: major, minor: minor, patch: patch) return Self(major: major, minor: minor, patch: patch)
} }
return nil return nil
} }
// MARK: Comparison Logic // MARK: Comparison Logic
internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func isSameAs(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major return self.major == version.major
&& self.minor == version.minor && self.minor == version.minor
&& (strict ? self.patch(strict, version) == version.patch(strict) : true) && (strict ? self.patch(strict, version) == version.patch(strict) : true)
} }
internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func isNewerThan(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return ( return (
self.major > version.major || self.major > version.major ||
@@ -163,7 +174,7 @@ public struct PhpVersionNumber: Equatable {
&& self.patch(strict) > version.patch(strict) && self.patch(strict) > version.patch(strict)
) )
} }
internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasNewerMinorVersionOrPatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && return self.major == version.major &&
( (
@@ -171,12 +182,12 @@ public struct PhpVersionNumber: Equatable {
|| self.minor > version.minor || self.minor > version.minor
) )
} }
internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasSameMajorAndMinorButNewerOrSamePatch(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major && self.minor == version.minor return self.major == version.major && self.minor == version.minor
&& self.patch(strict, version) >= version.patch(strict) && self.patch(strict, version) >= version.patch(strict)
} }
internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool { internal func hasSameMajorButNewerOrSameMinor(_ version: PhpVersionNumber, _ strict: Bool) -> Bool {
return self.major == version.major return self.major == version.major
&& self.minor >= version.minor && self.minor >= version.minor

View File

@@ -3,7 +3,7 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 31/01/2021. // Created by Nico Verbruggen on 31/01/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
@@ -16,24 +16,26 @@ import Foundation
instances. You can find more information here: https://nshipster.com/swift-regular-expressions/ instances. You can find more information here: https://nshipster.com/swift-regular-expressions/
*/ */
class PhpExtension { class PhpExtension {
/// The file where this extension was located. /// The file where this extension was located.
var file: String var file: String
/// The original string that was used to determine this extension is active. /// The original string that was used to determine this extension is active.
var line: String var line: String
/// The name of the extension. This is always identical to the name found in the original string. If you want to display this name, capitalize this. /// The name of the extension. This is always identical to the name found in the original string.
/// If you want to display this name, capitalize this.
var name: String var name: String
/// Whether the extension has been enabled. /// Whether the extension has been enabled.
var enabled: Bool var enabled: Bool
/// The file where this extension was located, but only the filename, not the full path to the .ini file. /// The file where this extension was located, but only the filename, not the full path to the .ini file.
var fileNameOnly: String { var fileNameOnly: String {
return String(file.split(separator: "/").last ?? "php.ini") return String(file.split(separator: "/").last ?? "php.ini")
} }
// swiftlint:disable line_length
/** /**
This regular expression will allow us to identify lines which activate an extension. This regular expression will allow us to identify lines which activate an extension.
@@ -47,29 +49,31 @@ class PhpExtension {
- Note: Extensions that are disabled in a different way will not be detected. This is intentional. - Note: Extensions that are disabled in a different way will not be detected. This is intentional.
*/ */
static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"# static let extensionRegex = #"^(extension|zend_extension|;(\s?)extension|;(\s?)zend_extension)(\s?)(=)(\s?)(?<name>["]?(?:\/?.\/?)+(?:\.so)"?)$"#
// swiftlint:enable line_length
/** /**
When registering an extension, we do that based on the line found inside the .ini file. When registering an extension, we do that based on the line found inside the .ini file.
*/ */
init(_ line: String, file: String) { init(_ line: String, file: String) {
let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: []) let regex = try! NSRegularExpression(pattern: Self.extensionRegex, options: [])
let match = regex.matches(in: line, options: [], range: NSMakeRange(0, line.count)).first let match = regex.matches(in: line, options: [], range: NSRange(location: 0, length: line.count)).first
let range = Range(match!.range(withName: "name"), in: line)! let range = Range(match!.range(withName: "name"), in: line)!
self.line = line self.line = line
let fullPath = String(line[range]) let fullPath = String(line[range])
.replacingOccurrences(of: "\"", with: "") // replace excess " .replacingOccurrences(of: "\"", with: "") // replace excess "
.replacingOccurrences(of: ".so", with: "") // replace excess .so .replacingOccurrences(of: ".so", with: "") // replace excess .so
self.name = String(fullPath.split(separator: "/").last!) // take last segment self.name = String(fullPath.split(separator: "/").last!) // take last segment
self.enabled = !line.contains(";") self.enabled = !line.contains(";")
self.file = file self.file = file
} }
/** /**
This simply toggles the extension in the .ini file. You may need to restart the other services in order for this change to apply. 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() {
let newLine = enabled let newLine = enabled
@@ -77,25 +81,25 @@ class PhpExtension {
? "; \(line)" ? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed // ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "") : line.replacingOccurrences(of: "; ", with: "")
sed(file: file, original: line, replacement: newLine) sed(file: file, original: line, replacement: newLine)
enabled.toggle() enabled.toggle()
} }
// MARK: - Static Methods // MARK: - Static Methods
/** /**
This method will attempt to identify all extensions in the .ini file at a certain URL. This method will attempt to identify all extensions in the .ini file at a certain URL.
*/ */
static func load(from path: URL) -> [PhpExtension] { static func load(from path: URL) -> [PhpExtension] {
let file = try? String(contentsOf: path, encoding: .utf8) let file = try? String(contentsOf: path, encoding: .utf8)
if (file == nil) { if file == nil {
Log.err("There was an issue reading the file. Assuming no extensions were found.") Log.err("There was an issue reading the file. Assuming no extensions were found.")
return [] return []
} }
return file!.components(separatedBy: "\n") return file!.components(separatedBy: "\n")
.filter { .filter {
return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil
@@ -104,5 +108,5 @@ class PhpExtension {
return PhpExtension($0, file: path.path) return PhpExtension($0, file: path.path)
} }
} }
} }

View File

@@ -3,35 +3,34 @@
// PHP Monitor // PHP Monitor
// //
// Created by Nico Verbruggen on 28/11/2021. // Created by Nico Verbruggen on 28/11/2021.
// Copyright © 2021 Nico Verbruggen. All rights reserved. // Copyright © 2022 Nico Verbruggen. All rights reserved.
// //
import Foundation import Foundation
class PhpInstallation { class PhpInstallation {
var longVersion: PhpVersionNumber var versionNumber: PhpVersionNumber
/** /**
In order to determine details about a PHP installation, well simply run `php-config --version` In order to determine details about a PHP installation, well simply run `php-config --version`
in the relevant directory. in the relevant directory.
*/ */
init(_ version: String) { init(_ version: String) {
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
self.longVersion = PhpVersionNumber.make(from: version)! self.versionNumber = PhpVersionNumber.make(from: version)!
if Shell.fileExists(phpConfigExecutablePath) { if Filesystem.fileExists(phpConfigExecutablePath) {
let longVersionString = Command.execute( let longVersionString = Command.execute(
path: phpConfigExecutablePath, path: phpConfigExecutablePath,
arguments: ["--version"] arguments: ["--version"]
).trimmingCharacters(in: .whitespacesAndNewlines) ).trimmingCharacters(in: .whitespacesAndNewlines)
// The parser should always work, or the string has to be very unusual. // 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. // If so, the app SHOULD crash, so that the users report what's up.
// TODO: Alert the user that the version number could not be parsed. self.versionNumber = try! PhpVersionNumber.parse(longVersionString)
self.longVersion = try! PhpVersionNumber.parse(longVersionString)
} }
} }
} }

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