Compare commits
151 Commits
Author | SHA1 | Date | |
---|---|---|---|
79430f7581 | |||
76f585a50e | |||
1bbc77967b | |||
1e4e2afe68 | |||
3caf90b0cd | |||
445acffa4c | |||
e90c068e95 | |||
c9428c300c | |||
807d9c55a4 | |||
ca167537cf | |||
8ae2031ba5 | |||
ec0ad13ad0 | |||
9988b775c9 | |||
8e24851014 | |||
c1d90cb909 | |||
4827c4a44b | |||
201554b237 | |||
e4bf6a9655 | |||
f9ea654ffc | |||
8677628850 | |||
e09b0156df | |||
41a83b1d91 | |||
a9e97b0bc9 | |||
e4e12799ef | |||
4e25fffa7d | |||
3e319cd50f | |||
595dc8c028 | |||
f7b1679e97 | |||
9f1761d68e | |||
871480d70c | |||
2b1c1c12f8 | |||
a22346ed35 | |||
e3fa34d4f9 | |||
3d225ea79f | |||
d2cd387c18 | |||
48bb782e33 | |||
9710ffa8da | |||
46408f5ee5 | |||
2c39f1db8b | |||
f20286cbd9 | |||
f1fe42e563 | |||
94abfe4b49 | |||
9778fd5c7b | |||
dba2ce5bf3 | |||
4644c1ada4 | |||
cef19243ee | |||
b319ecab59 | |||
a47b139d92 | |||
e026ecf60d | |||
3c0a4a6142 | |||
87ebb20284 | |||
d60c26c9b2 | |||
5c9c51f580 | |||
0c320074da | |||
e3ea712a99 | |||
4db478ca64 | |||
3064a07d69 | |||
f3e1b4de6f | |||
a3226b632f | |||
652878d97f | |||
032610ad5c | |||
2c2627dc9f | |||
62587bdf65 | |||
5e9dae78f5 | |||
949ba5b559 | |||
ce88f897ef | |||
fa9b51aaab | |||
b8affad5ee | |||
41e5f5b4c3 | |||
79f6a60a16 | |||
06bc4ddb9a | |||
bf728a24f0 | |||
b7cad3af62 | |||
4a3dee3c50 | |||
9d5a0ed745 | |||
b3b509409a | |||
4934f35d0b | |||
92e7418158 | |||
52ea64db40 | |||
f66e9b7340 | |||
2bf28fe247 | |||
c6e4f785bc | |||
94fe7df3bd | |||
f373621a4a | |||
5104a865fb | |||
7b10973330 | |||
bc208bddf9 | |||
321b4aaf8b | |||
b26fc3bc4b | |||
f758c5d63a | |||
c7510d778d | |||
70c5aadb7f | |||
a731f15cf7 | |||
ab4c436202 | |||
c0231690d4 | |||
988e9d3351 | |||
2f119d4332 | |||
d83c629a7b | |||
e7d98dbeae | |||
f3d5946743 | |||
7728a1125c | |||
3612351df7 | |||
8e912151fb | |||
3a2209e604 | |||
1f0b56cab6 | |||
e08d970edd | |||
32c757e711 | |||
480cdb94ae | |||
7fbcac5dc2 | |||
4edb5f5015 | |||
294f84ccb2 | |||
155b57eb9e | |||
a459f015e1 | |||
27676f13f4 | |||
b4b2d7052f | |||
6d25cf585e | |||
ba04c94c05 | |||
13447ba533 | |||
6f2e8f4b20 | |||
dc860074ef | |||
f586b8fcbe | |||
94714c3e7a | |||
904d05bdce | |||
ec30bee72b | |||
2fe3a4b7eb | |||
a7d5950aa0 | |||
e8306289ce | |||
23cf575026 | |||
94c84aaab3 | |||
9ca16e72d5 | |||
67a00f979a | |||
1e4c45dcbd | |||
87c44f3ae3 | |||
f39732a0e6 | |||
3b78ac43d7 | |||
1f19b81530 | |||
4dce6c033e | |||
72a8a1e382 | |||
ee050af364 | |||
f7e2551587 | |||
cc0cc21e5f | |||
883ea05bd1 | |||
641bddfce7 | |||
2f7223fba5 | |||
3b23ce7805 | |||
a634d083a6 | |||
9a3dd2fa22 | |||
6fd6241567 | |||
c8ab2e67f6 | |||
f82ab913c6 | |||
58943148fa |
4
.gitignore
vendored
@ -1,6 +1,4 @@
|
||||
phpmon.xcodeproj/project.xcworkspace
|
||||
phpmon.xcodeproj/xcuserdata
|
||||
PHP Monitor.xcodeproj/project.xcworkspace
|
||||
PHP Monitor.xcodeproj/xcuserdata
|
||||
phpmon-updater/PHP Monitor Self-Updater.app/
|
||||
|
||||
.DS_Store
|
||||
|
21
DEVELOPER.md
@ -14,6 +14,17 @@ It also automatically runs when you try to build the project. You'll get a warni
|
||||
swiftlint --fix
|
||||
```
|
||||
|
||||
## 📦 Swift Packages
|
||||
|
||||
Starting from PHP Monitor 7.1, the app now uses various first-party package dependencies.
|
||||
|
||||
The following package dependencies are in use:
|
||||
|
||||
* [`NVAppUpdater`](https://github.com/nicoverbruggen/NVAppUpdater)
|
||||
* [`NVAlert`](https://github.com/nicoverbruggen/NVAlert)
|
||||
|
||||
You may need an internet connection to download these dependencies, or you can also clone the dependencies and include them manually.
|
||||
|
||||
## ⚙️ Preferences
|
||||
|
||||
You can find the persisted configuration file in `~/Library/Preferences/com.nicoverbruggen.phpmon.plist`
|
||||
@ -33,16 +44,18 @@ defaults delete com.nicoverbruggen.phpmon && killall cfprefsd
|
||||
If you'd like to build PHP Monitor yourself, you need:
|
||||
|
||||
* Xcode (usually the latest version)
|
||||
* *PHP Monitor Self-Updater.app* in the `phpmon-updater` directory (You can build it yourself, it is included as a target OR copy the signed app so it is included w/ PHP Monitor)
|
||||
* The contents of this repository
|
||||
|
||||
Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you should be able to build the app for your system by pressing Cmd-R. This will create a debug build. (If Xcode complains about code signing, you can turn it off.)
|
||||
|
||||
**Important**: The updater now gets automatically built and included as part of the main target.
|
||||
|
||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||
|
||||
### PHP Monitor Updater
|
||||
## ✅ Testing
|
||||
|
||||
Select the separate target and build. You can then copy the product to the `phpmon-updater` directory. The binary will be re-signed when distributing the main build.
|
||||
In order to properly test everything, you will want to use the _PHP Monitor DEV_ target. There are unit and UI tests both.
|
||||
|
||||
You may sporadically see failures in UI tests due to the following error: `Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY`. This seems to be an issue with Xcode that Apple may need to resolve? You can retry the tests in question and they should eventually pass.
|
||||
|
||||
## 🚀 Release procedure
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
18
README.md
@ -1,7 +1,7 @@
|
||||
> **Note**
|
||||
> 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 [sponsoring](https://nicoverbruggen.be/sponsor) to support the project, as this is something I make in my free time. **Thank you!** ⭐️
|
||||
|
||||
<p align="center"><img src="./docs/logo.png" alt="PHP Monitor Logo" width="500px" /></p>
|
||||
|
||||
<p align="center"><img src="./docs/logo.svg" alt="PHP Monitor Logo" width="500px" /></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 if you want to use all of the functionality of the app</u> (consult the FAQ below with info about how to set up your environment).
|
||||
|
||||
@ -22,7 +22,7 @@ You can also add new domains as links, isolate sites, manage various services, a
|
||||
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 12.4 or later (Monterey, Ventura and Sonoma are supported)
|
||||
* macOS 13.5 or later
|
||||
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
|
||||
* Homebrew `php` formula is installed
|
||||
* Optional but recommended: Laravel Valet
|
||||
@ -84,9 +84,13 @@ Initially, I had an Alfred workflow for this — but it has now been replaced wi
|
||||
|
||||
## 🐘 Why not use Laravel Herd?
|
||||
|
||||
If you don't need to customize your local PHP setup and just want an easy and ready-to-go environment to start coding, [Laravel Herd](https://herd.laravel.com) is probably more than sufficient for many use cases.
|
||||
_**Disclaimer**: The author is not affiliated with Laravel or the Laravel team, nor Beyond Code, who maintain Laravel Herd. PHP Monitor is an independent project._
|
||||
|
||||
If you need more customization and flexibility I encourage you to consider PHP Monitor in combination with Laravel Valet or some other solution like Docker (with Laravel Sail, for example).
|
||||
If you don't need to customize your local PHP setup and just want an easy and ready-to-go environment to start coding, [Laravel Herd](https://herd.laravel.com) is probably more than sufficient for many use cases. They also offer paid features that may be useful to you or your team.
|
||||
|
||||
At this point, many people enjoy using Herd. However, Herd may not be for everyone, which is why other solutions to run PHP locally exist. If you need more customization and flexibility I encourage you to consider PHP Monitor in combination with Laravel Valet.
|
||||
|
||||
If you want to get as close as you can to a real server environment your best bet is probably to use a Docker container. I _highly_ recommend that you try different setups, and use what you like best.
|
||||
|
||||
## 🤬 The app won't start?!
|
||||
|
||||
@ -112,13 +116,15 @@ All stable and supported PHP versions are also supported by PHP Monitor. However
|
||||
|
||||
Backports that are installable via PHP Monitor's **PHP Version Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-php).
|
||||
|
||||
PHP extensions that are installable via PHP Monitor's **PHP Extension Manager** functionality are subject to availability via [this tap](https://github.com/shivammathur/homebrew-extensions).
|
||||
|
||||
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3. For more information, please see [SECURITY.md](./SECURITY.md) to find out which versions of PHP are supported with different versions of Valet.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
|
||||
|
||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
|
||||
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.3.
|
||||
|
||||
You can install other supported versions of PHP via PHP Monitor's **PHP Version Manager**. (You can manually install or upgrade PHP versions too, but this is not recommended.)
|
||||
|
||||
|
19
SECURITY.md
@ -2,21 +2,26 @@
|
||||
|
||||
## Supported versions
|
||||
|
||||
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 | Recommended Valet Version |
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 6.2 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 25 | ✅ Universal binary | ✅ Yes | Ventura (13.5+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+)<br/>Tahoe (26.0+)* | macOS 13.5+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
||||
(*) Denotes preliminary supported based on the app being built with the latest version of the SDK prior to the release of the latest release of macOS. Please check out the pinned issue for more information.
|
||||
|
||||
## Legacy versions
|
||||
|
||||
These versions of PHP Monitor are no longer supported, but if you’re using an older computer with an older version of Homebrew, Valet or macOS, you might want to use one of these versions.
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Deployment Target | Detected PHP Versions | Minimum Required Valet Version |
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4 | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 7.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.8 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.7 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 5.6 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0)<br/>Ventura (13.0) | macOS 11+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x) | 3.0 recommended<br/> 2.16.2 minimum |
|
||||
| 4.1 | ✅ Universal binary | ❌ | Big Sur (11.0)<br/>Monterey (12.0) | macOS 11+ | PHP 5.6—PHP 8.2 | 2.16.2 |
|
||||
|
BIN
assets/affinity/icon-unified.afdesign
Normal file
BIN
assets/affinity/logo.afdesign
Normal file
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
1
assets/icon-2025.svg
Normal file
After Width: | Height: | Size: 24 KiB |
10
assets/xcode-icon-composer/icon.icon/Assets/phpmon.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1.25033,-0.175723,0.175723,1.25033,14.4412,107.226)">
|
||||
<path d="M133.3,83.75L120.4,83.75L120.4,54.437C120.4,52.134 118.465,50.25 116.1,50.25L107.5,50.25C105.135,50.25 103.2,52.134 103.2,54.438L103.2,96.312C103.2,98.616 105.135,100.5 107.5,100.5L133.3,100.5C135.665,100.5 137.6,98.616 137.6,96.312L137.6,87.938C137.6,85.634 135.665,83.75 133.3,83.75ZM335.4,184.25L326.8,184.25L326.8,127.666C326.8,121.019 324.059,114.633 319.221,109.922L265.525,57.63C260.688,52.92 254.13,50.25 247.304,50.25L223.6,50.25L223.6,25.125C223.6,11.254 212.044,0 197.8,0L25.8,0C11.556,0 0,11.254 0,25.125L0,192.625C0,206.496 11.556,217.75 25.8,217.75L34.4,217.75C34.4,245.492 57.513,268 86,268C114.487,268 137.6,245.492 137.6,217.75L206.4,217.75C206.4,245.492 229.512,268 258,268C286.488,268 309.6,245.492 309.6,217.75L335.4,217.75C340.13,217.75 344,213.981 344,209.375L344,192.625C344,188.019 340.13,184.25 335.4,184.25ZM86,242.875C71.756,242.875 60.2,231.621 60.2,217.75C60.2,203.879 71.756,192.625 86,192.625C100.244,192.625 111.8,203.879 111.8,217.75C111.8,231.621 100.244,242.875 86,242.875ZM111.8,150.75C78.529,150.75 51.6,124.526 51.6,92.125C51.6,59.725 78.529,33.5 111.8,33.5C145.071,33.5 172,59.724 172,92.125C172,124.525 145.071,150.75 111.8,150.75ZM258,242.875C243.756,242.875 232.2,231.621 232.2,217.75C232.2,203.879 243.756,192.625 258,192.625C272.244,192.625 283.8,203.879 283.8,217.75C283.8,231.621 272.244,242.875 258,242.875ZM301,134L223.6,134L223.6,75.375L247.304,75.375L301,127.666L301,134Z" style="fill:url(#_Linear1);fill-rule:nonzero;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.96882e-14,321.533,-321.533,1.96882e-14,172.079,-41.4918)"><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="0" style="stop-color:rgb(81,194,251);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(28,145,254);stop-opacity:1"/></linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
48
assets/xcode-icon-composer/icon.icon/icon.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:0.27800,0.58000,0.98800,1.00000",
|
||||
"srgb:0.27800,0.58000,0.98800,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"blend-mode" : "screen",
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : {
|
||||
"solid" : "srgb:1.00000,0.99038,0.96423,1.00000"
|
||||
},
|
||||
"glass" : true,
|
||||
"image-name" : "phpmon.svg",
|
||||
"name" : "phpmon",
|
||||
"position" : {
|
||||
"scale" : 1.85,
|
||||
"translation-in-points" : [
|
||||
10.0234375,
|
||||
8.21875
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"specular" : true,
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
BIN
docs/logo.png
Before Width: | Height: | Size: 51 KiB |
57
docs/logo.svg
Normal file
After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 723 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 811 B After Width: | Height: | Size: 811 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 131 KiB |
Before Width: | Height: | Size: 301 KiB After Width: | Height: | Size: 450 KiB |
@ -1,46 +0,0 @@
|
||||
//
|
||||
// LaunchControl.swift
|
||||
// PHP Monitor Self-Updater
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class LaunchControl {
|
||||
public static func smartRestart(priority: [String]) async {
|
||||
for appPath in priority {
|
||||
if FileManager.default.fileExists(atPath: appPath) {
|
||||
let app = await LaunchControl.startApplication(at: appPath)
|
||||
if app != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func terminateApplications(bundleIds: [String]) async {
|
||||
let runningApplications = NSWorkspace.shared.runningApplications
|
||||
|
||||
// Terminate all instances found
|
||||
for id in bundleIds {
|
||||
if let phpmon = runningApplications.first(where: {
|
||||
(application) in return application.bundleIdentifier == id
|
||||
}) {
|
||||
phpmon.terminate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func startApplication(at path: String) async -> NSRunningApplication? {
|
||||
await withCheckedContinuation { continuation in
|
||||
let url = NSURL(fileURLWithPath: path, isDirectory: true) as URL
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
NSWorkspace.shared.openApplication(at: url, configuration: configuration) { phpmon, error in
|
||||
continuation.resume(returning: phpmon)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
//
|
||||
// Updater.swift
|
||||
// PHP Monitor Updater
|
||||
//
|
||||
// Created by Nico Verbruggen on 01/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
class Updater: NSObject, NSApplicationDelegate {
|
||||
|
||||
var updaterDirectory: String = ""
|
||||
var manifestPath: String = ""
|
||||
var manifest: ReleaseManifest! = nil
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
Task { await self.installUpdate() }
|
||||
}
|
||||
|
||||
func installUpdate() async {
|
||||
print("PHP MONITOR SELF-UPDATER by Nico Verbruggen")
|
||||
print("===========================================")
|
||||
|
||||
self.updaterDirectory = "~/.config/phpmon/updater"
|
||||
.replacingOccurrences(of: "~", with: NSHomeDirectory())
|
||||
|
||||
print("Updater directory set to: \(self.updaterDirectory)")
|
||||
|
||||
self.manifestPath = "\(updaterDirectory)/update.json"
|
||||
|
||||
// Fetch the manifest on the local filesystem
|
||||
let manifest = await parseManifest()!
|
||||
|
||||
// Download the latest file
|
||||
let zipPath = await download(manifest)
|
||||
|
||||
// Terminate all instances of PHP Monitor first
|
||||
await LaunchControl.terminateApplications(bundleIds: [
|
||||
"com.nicoverbruggen.phpmon.eap",
|
||||
"com.nicoverbruggen.phpmon.dev",
|
||||
"com.nicoverbruggen.phpmon"
|
||||
])
|
||||
|
||||
// Install the app based on the zip
|
||||
let appPath = await extractAndInstall(zipPath: zipPath)
|
||||
|
||||
// Restart PHP Monitor, this will also close the updater
|
||||
_ = await LaunchControl.startApplication(at: appPath)
|
||||
|
||||
exit(1)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
private func parseManifest() async -> ReleaseManifest? {
|
||||
// Read out the correct information from the manifest JSON
|
||||
print("Checking manifest file at \(manifestPath)...")
|
||||
|
||||
do {
|
||||
let manifestText = try String(contentsOfFile: manifestPath)
|
||||
manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!)
|
||||
return manifest
|
||||
} catch {
|
||||
print("Parsing the manifest failed (or the manifest file doesn't exist)!")
|
||||
await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
private func download(_ manifest: ReleaseManifest) async -> String {
|
||||
// Remove all zips
|
||||
system_quiet("rm -rf \(updaterDirectory)/*.zip")
|
||||
|
||||
// Download the file (and follow redirects + no output on failure)
|
||||
system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20")
|
||||
|
||||
// Identify the downloaded file
|
||||
let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Ensure the zip exists
|
||||
if filename.isEmpty {
|
||||
print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!")
|
||||
await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)")
|
||||
}
|
||||
|
||||
// Calculate the checksum for the downloaded file
|
||||
let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
// Compare the checksums
|
||||
print("""
|
||||
Comparing checksums...
|
||||
Expected SHA256: \(manifest.sha256)
|
||||
Actual SHA256: \(checksum)
|
||||
""")
|
||||
|
||||
// Make sure the checksum matches before we do anything with the file
|
||||
if checksum != manifest.sha256 {
|
||||
print("The checksums failed to match. Cancelling!")
|
||||
await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.")
|
||||
}
|
||||
|
||||
// Return the path to the zip
|
||||
return "\(updaterDirectory)/\(filename)"
|
||||
}
|
||||
|
||||
private func extractAndInstall(zipPath: String) async -> String {
|
||||
// Remove the directory that will contain the extracted update
|
||||
system_quiet("rm -rf \"\(updaterDirectory)/extracted\"")
|
||||
|
||||
// Recreate the directory where we will unzip the .app file
|
||||
system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"")
|
||||
|
||||
// Make sure the updater directory exists
|
||||
var isDirectory: ObjCBool = true
|
||||
if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) {
|
||||
await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
||||
}
|
||||
|
||||
// Unzip the file
|
||||
system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"")
|
||||
|
||||
// Find the .app file
|
||||
let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
print("Finished extracting: \(updaterDirectory)/extracted/\(app)")
|
||||
|
||||
// Make sure the file was extracted
|
||||
if app.isEmpty {
|
||||
await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
||||
}
|
||||
|
||||
// Remove the original app
|
||||
print("Removing \(app) before replacing...")
|
||||
system_quiet("rm -rf \"/Applications/\(app)\"")
|
||||
|
||||
// Move the new app in place
|
||||
system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"")
|
||||
|
||||
// Remove the zip
|
||||
system_quiet("rm \"\(zipPath)\"")
|
||||
|
||||
// Remove the manifest
|
||||
system_quiet("rm \"\(manifestPath)\"")
|
||||
|
||||
// Write a file that is only written when we upgraded successfully
|
||||
system_quiet("touch \"\(updaterDirectory)/upgrade.success\"")
|
||||
|
||||
// Return the new location of the app
|
||||
return "/Applications/\(app)"
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
//
|
||||
// Utility.swift
|
||||
// PHP Monitor Self-Updater
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/02/2023.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Alert {
|
||||
public static func show(description: String, shouldExit: Bool = true) async {
|
||||
await withUnsafeContinuation { continuation in
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "The app could not be updated."
|
||||
alert.informativeText = description
|
||||
alert.addButton(withTitle: "OK")
|
||||
alert.alertStyle = .critical
|
||||
alert.runModal()
|
||||
if shouldExit {
|
||||
exit(0)
|
||||
}
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReleaseManifest: Codable {
|
||||
let url: String
|
||||
let sha256: String
|
||||
}
|
@ -7,8 +7,17 @@
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import NVAppUpdater
|
||||
|
||||
let app = NSApplication.shared
|
||||
let delegate = Updater()
|
||||
app.delegate = delegate
|
||||
let delegate = SelfUpdater(
|
||||
appName: "PHP Monitor",
|
||||
bundleIdentifiers: [
|
||||
"com.nicoverbruggen.phpmon.eap",
|
||||
"com.nicoverbruggen.phpmon.dev",
|
||||
"com.nicoverbruggen.phpmon"
|
||||
],
|
||||
selfUpdaterPath: "~/.config/phpmon/updater"
|
||||
)
|
||||
|
||||
NSApplication.shared.delegate = delegate
|
||||
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
|
||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 558 B After Width: | Height: | Size: 783 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 126 KiB |
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 457 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 632 B After Width: | Height: | Size: 790 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 182 KiB |
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 648 KiB |
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 819 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 499 KiB |
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "1.000",
|
||||
"green" : "1.000",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.172",
|
||||
"green" : "0.182",
|
||||
"red" : "0.182"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.988",
|
||||
"green" : "0.723",
|
||||
"green" : "0.444",
|
||||
"red" : "0.277"
|
||||
}
|
||||
},
|
||||
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.300",
|
||||
"blue" : "0.180",
|
||||
"green" : "0.841",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.300",
|
||||
"blue" : "0.426",
|
||||
"green" : "0.809",
|
||||
"red" : "1.000"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
25
phpmon/Assets.xcassets/php.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "php.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
1
phpmon/Assets.xcassets/php.imageset/php.svg
vendored
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="800" height="800" viewBox="0 0 24 24"><path d="M7.01 10.207h-.944l-.515 2.648h.838c.556 0 .97-.105 1.242-.314.272-.21.455-.559.55-1.049.092-.47.05-.802-.124-.995-.175-.193-.523-.29-1.047-.29zM12 5.688C5.373 5.688 0 8.514 0 12s5.373 6.313 12 6.313S24 15.486 24 12c0-3.486-5.373-6.312-12-6.312zm-3.26 7.451c-.261.25-.575.438-.917.551-.336.108-.765.164-1.285.164H5.357l-.327 1.681H3.652l1.23-6.326h2.65c.797 0 1.378.209 1.744.628.366.418.476 1.002.33 1.752a2.836 2.836 0 0 1-.305.847c-.143.255-.33.49-.561.703zm4.024.715.543-2.799c.063-.318.039-.536-.068-.651-.107-.116-.336-.174-.687-.174H11.46l-.704 3.625H9.388l1.23-6.327h1.367l-.327 1.682h1.218c.767 0 1.295.134 1.586.401s.378.7.263 1.299l-.572 2.944h-1.389zm7.597-2.265a2.782 2.782 0 0 1-.305.847c-.143.255-.33.49-.561.703a2.44 2.44 0 0 1-.917.551c-.336.108-.765.164-1.286.164h-1.18l-.327 1.682h-1.378l1.23-6.326h2.649c.797 0 1.378.209 1.744.628.366.417.477 1.001.331 1.751zm-2.595-1.382h-.943l-.516 2.648h.838c.557 0 .971-.105 1.242-.314.272-.21.455-.559.551-1.049.092-.47.049-.802-.125-.995s-.524-.29-1.047-.29z"/></svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -14,14 +14,17 @@ class Actions {
|
||||
|
||||
public static func linkPhp() async {
|
||||
await brew("link php --overwrite --force")
|
||||
|
||||
// TODO: Verify that this worked, if not, notify the user
|
||||
}
|
||||
|
||||
public static func restartPhpFpm() async {
|
||||
await brew("services restart \(HomebrewFormulae.php)", sudo: HomebrewFormulae.php.elevated)
|
||||
}
|
||||
|
||||
public static func restartPhpFpm(version: String) async {
|
||||
let formula = (version == PhpEnvironments.brewPhpAlias) ? "php" : "php@\(version)"
|
||||
await brew("services restart \(formula)", sudo: HomebrewFormulae.php.elevated)
|
||||
}
|
||||
|
||||
public static func restartNginx() async {
|
||||
await brew("services restart \(HomebrewFormulae.nginx)", sudo: HomebrewFormulae.nginx.elevated)
|
||||
}
|
||||
|
@ -18,6 +18,20 @@ struct Constants {
|
||||
*/
|
||||
static let MinimumRecommendedValetVersion = "2.16.2"
|
||||
|
||||
/**
|
||||
PHP Monitor supplies a hardcoded list of PHP packages in its own
|
||||
PHP Version Manager.
|
||||
|
||||
This hardcoded list will expire and will need to be modified when
|
||||
the cutoff date occurs, which is when the `php` formula will
|
||||
become PHP 8.5, and a new build will need to be made.
|
||||
|
||||
If users launch an older version of the app, then a warning
|
||||
will be displayed to let them know that certain operations
|
||||
will not work correctly and that they need to update their app.
|
||||
*/
|
||||
static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD
|
||||
|
||||
/**
|
||||
* The PHP versions that are considered pre-release versions.
|
||||
* Past a certain date, an experimental version "graduates"
|
||||
@ -25,8 +39,8 @@ struct Constants {
|
||||
*/
|
||||
static var ExperimentalPhpVersions: Set<String> {
|
||||
let releaseDates = [
|
||||
"8.4": Date.fromString("2024-12-01"), // PLACEHOLDER DATE
|
||||
"8.3": Date.fromString("2023-11-23") // OFFICIAL RELEASE
|
||||
"8.5": Date.fromString(Self.PhpFormulaeCutoffDate),
|
||||
"8.4": Date.fromString("2024-11-22")
|
||||
]
|
||||
|
||||
return Set(releaseDates
|
||||
@ -41,6 +55,17 @@ struct Constants {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
The Homebrew services that should be automatically
|
||||
detected and show up in the list of managed services.
|
||||
*/
|
||||
static let DetectedHomebrewServices: Set = [
|
||||
"mailhog",
|
||||
"mysql@",
|
||||
"postgresql@",
|
||||
"redis"
|
||||
]
|
||||
|
||||
/**
|
||||
* The PHP versions supported by this application.
|
||||
* Any other PHP versions are considered invalid.
|
||||
@ -48,9 +73,8 @@ struct Constants {
|
||||
static let DetectedPhpVersions: Set = [
|
||||
"5.6",
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3",
|
||||
"8.4"
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4",
|
||||
"8.5" // DEV
|
||||
]
|
||||
|
||||
/**
|
||||
@ -66,14 +90,13 @@ struct Constants {
|
||||
3: // Valet v3 dropped support for v5.6
|
||||
[
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3", "8.4" // dev
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||
],
|
||||
4: // Valet v4 dropped support for v7.0
|
||||
[
|
||||
"7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2",
|
||||
"8.3", "8.4" // dev
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4",
|
||||
"8.5" // DEV
|
||||
]
|
||||
]
|
||||
|
||||
@ -89,6 +112,14 @@ struct Constants {
|
||||
string: "https://phpmon.app/faq"
|
||||
)!
|
||||
|
||||
static let WikiPhpUnavailable = URL(
|
||||
string: "https://phpmon.app/php-unavailable"
|
||||
)!
|
||||
|
||||
static let WikiPhpUpgrade = URL(
|
||||
string: "https://phpmon.app/php-upgrade"
|
||||
)!
|
||||
|
||||
static let DonationPayment = URL(
|
||||
string: "https://phpmon.app/sponsor/now"
|
||||
)!
|
||||
|
@ -45,7 +45,6 @@ func grepContains(file: String, query: String) async -> Bool {
|
||||
|
||||
/**
|
||||
Attempts to introduce sleep for a particular duration. Use with caution.
|
||||
Only intended for testing purposes.
|
||||
*/
|
||||
func delay(seconds: Double) async {
|
||||
try! await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||
|
@ -102,6 +102,14 @@ public class Paths {
|
||||
return "\(shared.baseDir.rawValue)/etc"
|
||||
}
|
||||
|
||||
public static var tapPath: String {
|
||||
if shared.baseDir == .usr {
|
||||
return "\(shared.baseDir.rawValue)/homebrew/Library/Taps"
|
||||
}
|
||||
|
||||
return "\(shared.baseDir.rawValue)/Library/Taps"
|
||||
}
|
||||
|
||||
public static var caskroomPath: String {
|
||||
return "\(shared.baseDir.rawValue)/Caskroom/"
|
||||
+ (App.identifier.contains(".dev") ? "phpmon-dev" : "phpmon")
|
||||
|
@ -8,6 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol AlertableError {
|
||||
public protocol AlertableError {
|
||||
func getErrorMessageKey() -> String
|
||||
}
|
||||
|
@ -9,6 +9,24 @@
|
||||
import Cocoa
|
||||
|
||||
extension NSMenuItem {
|
||||
convenience init(
|
||||
title: String,
|
||||
action: Selector? = nil,
|
||||
keyEquivalent: String = "",
|
||||
keyModifier: NSEvent.ModifierFlags = [],
|
||||
systemImage: String? = nil,
|
||||
customImage: String? = nil,
|
||||
) {
|
||||
self.init(title: title, action: action, keyEquivalent: keyEquivalent)
|
||||
self.keyEquivalentModifierMask = keyModifier
|
||||
if systemImage != nil {
|
||||
self.image = NSImage(systemSymbolName: systemImage!, accessibilityDescription: "")
|
||||
}
|
||||
if customImage != nil {
|
||||
self.image = NSImage(named: customImage!)
|
||||
}
|
||||
}
|
||||
|
||||
convenience init(
|
||||
title: String,
|
||||
action: Selector? = nil,
|
||||
@ -26,12 +44,20 @@ extension NSMenuItem {
|
||||
keyEquivalent: String = "",
|
||||
keyModifier: NSEvent.ModifierFlags = [],
|
||||
toolTip: String? = nil,
|
||||
systemImage: String? = nil,
|
||||
customImage: String? = nil,
|
||||
submenu: [NSMenuItem],
|
||||
target: NSObject? = nil
|
||||
) {
|
||||
self.init(title: title, action: nil, keyEquivalent: keyEquivalent)
|
||||
self.keyEquivalentModifierMask = keyModifier
|
||||
self.toolTip = toolTip
|
||||
if systemImage != nil {
|
||||
self.image = NSImage(systemSymbolName: systemImage!, accessibilityDescription: "")
|
||||
}
|
||||
if customImage != nil {
|
||||
self.image = NSImage(named: customImage!)
|
||||
}
|
||||
self.submenu = NSMenu(items: submenu, target: target)
|
||||
}
|
||||
}
|
||||
|
23
phpmon/Common/Extensions/NVAlertExtension.swift
Normal file
@ -0,0 +1,23 @@
|
||||
//
|
||||
// NVAlertExtension.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 16/07/2024.
|
||||
// Copyright © 2024 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NVAlert
|
||||
|
||||
extension NVAlert {
|
||||
/**
|
||||
Shows the modal for a particular error.
|
||||
*/
|
||||
@MainActor public static func show(for error: Error & AlertableError) {
|
||||
let key = error.getErrorMessageKey()
|
||||
return NVAlert().withInformation(
|
||||
title: "\(key).title".localized,
|
||||
subtitle: "\(key).description".localized
|
||||
).withPrimary(text: "generic.ok".localized).show()
|
||||
}
|
||||
}
|
@ -8,6 +8,18 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct Localization {
|
||||
static var preferredLanguage: String? {
|
||||
guard let language = Preferences.preferences[.languageOverride] as? String else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if language.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
return language
|
||||
}
|
||||
|
||||
static var bundle: Bundle = {
|
||||
if !isRunningTests {
|
||||
return Bundle.main
|
||||
@ -32,7 +44,15 @@ struct Localization {
|
||||
|
||||
extension String {
|
||||
var localized: String {
|
||||
let string = NSLocalizedString(self, tableName: nil, bundle: Localization.bundle, value: "", comment: "")
|
||||
var preferredBundle: Bundle = Localization.bundle
|
||||
|
||||
if let preferred = Localization.preferredLanguage,
|
||||
let path = Localization.bundle.path(forResource: preferred, ofType: "lproj"),
|
||||
let bundle = Bundle(path: path) {
|
||||
preferredBundle = bundle
|
||||
}
|
||||
|
||||
let string = NSLocalizedString(self, tableName: nil, bundle: preferredBundle, value: "", comment: "")
|
||||
|
||||
// Fallback to English translation if the localized value is equal to the key (should not happen)
|
||||
if string == self {
|
||||
|
@ -64,7 +64,7 @@ class RealFileSystem: FileSystemProtocol {
|
||||
// MARK: — FS Attributes
|
||||
|
||||
func makeExecutable(_ path: String) throws {
|
||||
_ = system("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
_ = ActiveShell.shared.sync("chmod +x \(path.replacingTildeWithHomeDirectory)")
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
|
@ -75,7 +75,7 @@ class MenuBarImageGenerator {
|
||||
|
||||
// Then we'll fetch the image we want on the left
|
||||
var iconType = Preferences.preferences[.iconTypeToDisplay] as? String
|
||||
if iconType == nil {
|
||||
if iconType == nil || !MenuBarIcon.allCases.map({ $0.rawValue }).contains(iconType) {
|
||||
Log.warn("Invalid icon type found, using the default")
|
||||
iconType = MenuBarIcon.iconPhp.rawValue
|
||||
}
|
||||
|
@ -29,6 +29,8 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
App.shared.remove(window: windowName)
|
||||
}
|
||||
|
||||
func windowDidResize(_ notification: Notification) {}
|
||||
|
||||
deinit {
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ import Foundation
|
||||
|
||||
/**
|
||||
Run a simple blocking Shell command on the user's own system.
|
||||
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
|
||||
*/
|
||||
public func system(_ command: String) -> String {
|
||||
let task = Process()
|
||||
|
@ -1,17 +0,0 @@
|
||||
//
|
||||
// WIP.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 01/11/2022.
|
||||
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func todo(_ context: String = "") {
|
||||
if !context.isEmpty {
|
||||
fatalError("To be implemented: \(context)")
|
||||
}
|
||||
|
||||
fatalError("To be implemented")
|
||||
}
|
@ -62,13 +62,6 @@ class ActivePhpInstallation {
|
||||
return
|
||||
}
|
||||
|
||||
// Load extension information
|
||||
let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini")
|
||||
|
||||
if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) {
|
||||
iniFiles.append(file)
|
||||
}
|
||||
|
||||
// Get configuration values
|
||||
limits = Limits(
|
||||
memory_limit: getByteCount(key: "memory_limit"),
|
||||
@ -76,15 +69,10 @@ class ActivePhpInstallation {
|
||||
post_max_size: getByteCount(key: "post_max_size")
|
||||
)
|
||||
|
||||
// Return a list of .ini files parsed after php.ini
|
||||
let paths = Command.execute(
|
||||
path: Paths.php,
|
||||
arguments: ["-r", "echo php_ini_scanned_files();"],
|
||||
trimNewlines: false
|
||||
)
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.split(separator: ",")
|
||||
.map { String($0) }
|
||||
let paths = ActiveShell.shared
|
||||
.sync("\(Paths.php) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||
.split(separator: "\n")
|
||||
.map { String($0) }
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
|
@ -87,7 +87,14 @@ class PhpEnvironments {
|
||||
var cachedPhpInstallations: [String: PhpInstallation] = [:]
|
||||
|
||||
/** Information about the currently linked PHP installation. */
|
||||
var currentInstall: ActivePhpInstallation?
|
||||
var currentInstall: ActivePhpInstallation? {
|
||||
didSet {
|
||||
// Let the PHP extension manager, if it exists, know the version changed
|
||||
if let version = currentInstall?.version.short {
|
||||
App.shared.phpExtensionManagerWindowController?.view?.manager.phpVersion = version
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
The version that the `php` formula via Brew is aliased to on the current system.
|
||||
@ -173,7 +180,7 @@ class PhpEnvironments {
|
||||
let phpAliasInstall = PhpInstallation(phpAlias)
|
||||
// Before inserting, ensure that the actual output matches the alias
|
||||
// if that isn't the case, our formula remains out-of-date
|
||||
if !phpAliasInstall.missingBinary {
|
||||
if !phpAliasInstall.isMissingBinary {
|
||||
supportedVersions.insert(phpAlias)
|
||||
}
|
||||
}
|
||||
|
@ -12,21 +12,48 @@ class PhpInstallation {
|
||||
|
||||
var versionNumber: VersionNumber
|
||||
|
||||
var missingBinary: Bool = false
|
||||
var iniFiles: [PhpConfigurationFile] = []
|
||||
|
||||
var isPreRelease: Bool = false
|
||||
|
||||
var isMissingBinary: Bool = false
|
||||
|
||||
var isHealthy: Bool = true
|
||||
|
||||
var extensions: [PhpExtension] {
|
||||
return self.iniFiles.flatMap({ $0.extensions })
|
||||
}
|
||||
|
||||
var formulaName: String {
|
||||
let version = self.versionNumber.short
|
||||
|
||||
if version == PhpEnvironments.brewPhpAlias {
|
||||
return "php"
|
||||
}
|
||||
|
||||
return "php@\(self.versionNumber.short)"
|
||||
}
|
||||
|
||||
/**
|
||||
In order to determine details about a PHP installation,
|
||||
we’ll simply run `php-config --version` in the relevant directory.
|
||||
*/
|
||||
init(_ version: String) {
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config"
|
||||
let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config",
|
||||
phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||
|
||||
let phpExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php"
|
||||
versionNumber = VersionNumber.make(from: version)!
|
||||
|
||||
self.versionNumber = VersionNumber.make(from: version)!
|
||||
determineVersion(phpConfigExecutablePath, phpExecutablePath)
|
||||
determineHealth(phpExecutablePath)
|
||||
determineIniFiles(phpExecutablePath)
|
||||
|
||||
// Find all enabled extensions
|
||||
let enabled = self.extensions.filter({ $0.enabled }).map({ $0.name })
|
||||
Log.info("PHP \(versionNumber.short) has the following extensions enabled: \(enabled)")
|
||||
}
|
||||
|
||||
private func determineVersion(_ phpConfigExecutablePath: String, _ phpExecutablePath: String) {
|
||||
if FileSystem.fileExists(phpConfigExecutablePath) {
|
||||
let longVersionString = Command.execute(
|
||||
path: phpConfigExecutablePath,
|
||||
@ -34,15 +61,21 @@ class PhpInstallation {
|
||||
trimNewlines: false
|
||||
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if longVersionString.contains("-dev") {
|
||||
isPreRelease = true
|
||||
}
|
||||
|
||||
// The parser should always work, or the string has to be very unusual.
|
||||
// If so, the app SHOULD crash, so that the users report what's up.
|
||||
self.versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
} else {
|
||||
// Keep track that the `php-config` binary is missing; this often means there's a mismatch between
|
||||
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||
missingBinary = true
|
||||
// the `php` version alias and the actual installed version (e.g. you haven't upgraded `php`)
|
||||
isMissingBinary = true
|
||||
}
|
||||
}
|
||||
|
||||
private func determineHealth(_ phpExecutablePath: String) {
|
||||
if FileSystem.fileExists(phpExecutablePath) {
|
||||
let testCommand = Command.execute(
|
||||
path: phpExecutablePath,
|
||||
@ -59,4 +92,18 @@ class PhpInstallation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func determineIniFiles(_ phpExecutablePath: String) {
|
||||
let paths = ActiveShell.shared
|
||||
.sync("\(phpExecutablePath) --ini | grep -E -o '(/[^ ]+\\.ini)'").out
|
||||
.split(separator: "\n")
|
||||
.map { String($0) }
|
||||
|
||||
// See if any extensions are present in said .ini files
|
||||
paths.forEach { (iniFilePath) in
|
||||
if let file = PhpConfigurationFile.from(filePath: iniFilePath) {
|
||||
iniFiles.append(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ extension InternalSwitcher {
|
||||
return corrections.contains(true)
|
||||
}
|
||||
|
||||
// MARK: - PHP FPM pool
|
||||
// MARK: - Corrections
|
||||
|
||||
public func disableDefaultPhpFpmPool(_ version: String) async -> FixApplied {
|
||||
let pool = "\(Paths.etcPath)/php/\(version)/php-fpm.d/www.conf"
|
||||
@ -54,37 +54,7 @@ extension InternalSwitcher {
|
||||
return false
|
||||
}
|
||||
|
||||
func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||
return [
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/php-fpm.d/valet-fpm.conf",
|
||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||
],
|
||||
applies: { Valet.shared.version!.major > 2 }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/error_log.ini",
|
||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||
],
|
||||
applies: { return true }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/php-memory-limits.ini",
|
||||
source: "/cli/stubs/php-memory-limits.ini",
|
||||
replacements: [:],
|
||||
applies: { return true }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
public func ensureConfigurationFilesExist(_ version: String) async -> FixApplied {
|
||||
let files = self.getExpectedConfigurationFiles(for: version)
|
||||
|
||||
// For each of the files, attempt to fix anything that is wrong
|
||||
@ -124,6 +94,38 @@ extension InternalSwitcher {
|
||||
return outcomes.contains(true)
|
||||
}
|
||||
|
||||
// MARK: - Internals
|
||||
|
||||
private func getExpectedConfigurationFiles(for version: String) -> [ExpectedConfigurationFile] {
|
||||
return [
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/php-fpm.d/valet-fpm.conf",
|
||||
source: "/cli/stubs/etc-phpfpm-valet.conf",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory,
|
||||
"valet.sock": "valet\(version.replacingOccurrences(of: ".", with: "")).sock"
|
||||
],
|
||||
applies: { Valet.shared.version!.major > 2 }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/error_log.ini",
|
||||
source: "/cli/stubs/etc-phpfpm-error_log.ini",
|
||||
replacements: [
|
||||
"VALET_USER": Paths.whoami,
|
||||
"VALET_HOME_PATH": "~/.config/valet".replacingTildeWithHomeDirectory
|
||||
],
|
||||
applies: { return true }
|
||||
),
|
||||
ExpectedConfigurationFile(
|
||||
destination: "/conf.d/php-memory-limits.ini",
|
||||
source: "/cli/stubs/php-memory-limits.ini",
|
||||
replacements: [:],
|
||||
applies: { return true }
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct ExpectedConfigurationFile {
|
||||
|
@ -8,9 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Process: @unchecked Sendable {}
|
||||
extension Timer: @unchecked Sendable {}
|
||||
|
||||
class RealShell: ShellProtocol {
|
||||
/**
|
||||
The launch path of the terminal in question that is used.
|
||||
@ -86,14 +83,37 @@ class RealShell: ShellProtocol {
|
||||
|
||||
// MARK: - Shellable Protocol
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||
sleep(3)
|
||||
}
|
||||
|
||||
task.standardOutput = outputPipe
|
||||
task.standardError = errorPipe
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
func pipe(_ command: String) async -> ShellOutput {
|
||||
let task = getShellProcess(for: command)
|
||||
|
||||
let outputPipe = Pipe()
|
||||
let errorPipe = Pipe()
|
||||
|
||||
// Seriously slow down how long it takes for the shell to return output
|
||||
// (in order to debug or identify async issues)
|
||||
if ProcessInfo.processInfo.environment["SLOW_SHELL_MODE"] != nil {
|
||||
Log.info("[SLOW SHELL] \(command)")
|
||||
await delay(seconds: 3.0)
|
||||
@ -104,20 +124,20 @@ class RealShell: ShellProtocol {
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
let stdOut = String(
|
||||
data: outputPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
|
||||
let stdErr = String(
|
||||
data: errorPipe.fileHandleForReading.readDataToEndOfFile(),
|
||||
encoding: .utf8
|
||||
)!
|
||||
let stdOut = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
let stdErr = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)!
|
||||
|
||||
if Log.shared.verbosity == .cli {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
log(task: task, stdOut: stdOut, stdErr: stdErr)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
}
|
||||
|
||||
private func log(task: Process, stdOut: String, stdErr: String) {
|
||||
var args = task.arguments ?? []
|
||||
let last = "\"" + (args.popLast() ?? "") + "\""
|
||||
var log = """
|
||||
|
||||
<~~~~~~~~~~~~~~~~~~~~~~~
|
||||
$ \(([self.launchPath] + args + [last]).joined(separator: " "))
|
||||
@ -126,22 +146,19 @@ class RealShell: ShellProtocol {
|
||||
\(stdOut)
|
||||
"""
|
||||
|
||||
if !stdErr.isEmpty {
|
||||
log.append("""
|
||||
if !stdErr.isEmpty {
|
||||
log.append("""
|
||||
[ERR]:
|
||||
\(stdErr)
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
log.append("""
|
||||
log.append("""
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~>
|
||||
|
||||
""")
|
||||
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
return .out(stdOut, stdErr)
|
||||
Log.info(log)
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
@ -164,25 +181,26 @@ class RealShell: ShellProtocol {
|
||||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation({ continuation in
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { _ in
|
||||
let task = Task {
|
||||
try await Task.sleep(nanoseconds: timeout.nanoseconds)
|
||||
// Only terminate if the process is still running
|
||||
if process.isRunning {
|
||||
process.terminationHandler = nil
|
||||
process.terminate()
|
||||
return continuation.resume(throwing: ShellError.timedOut)
|
||||
continuation.resume(throwing: ShellError.timedOut)
|
||||
}
|
||||
}
|
||||
|
||||
process.terminationHandler = { [timer, output] process in
|
||||
timer.invalidate()
|
||||
process.terminationHandler = { [output] process in
|
||||
task.cancel()
|
||||
|
||||
process.haltListening()
|
||||
|
||||
if !output.err.isEmpty {
|
||||
return continuation.resume(returning: (process, .err(output.err)))
|
||||
continuation.resume(returning: (process, .err(output.err)))
|
||||
} else {
|
||||
continuation.resume(returning: (process, .out(output.out)))
|
||||
}
|
||||
|
||||
return continuation.resume(returning: (process, .out(output.out)))
|
||||
}
|
||||
|
||||
process.launch()
|
||||
@ -190,3 +208,9 @@ class RealShell: ShellProtocol {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
extension TimeInterval {
|
||||
var nanoseconds: UInt64 {
|
||||
return UInt64(self * 1_000_000_000)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,16 @@ protocol ShellProtocol {
|
||||
*/
|
||||
var PATH: String { get }
|
||||
|
||||
/**
|
||||
Run a command synchronously. Use with caution.
|
||||
|
||||
Common usage:
|
||||
```
|
||||
let output = Shell.sync("php -v")
|
||||
```
|
||||
*/
|
||||
func sync(_ command: String) -> ShellOutput
|
||||
|
||||
/**
|
||||
Run a command asynchronously.
|
||||
Returns the most relevant output (prefers error output if it exists).
|
||||
|
@ -1,5 +1,5 @@
|
||||
//
|
||||
// PhpFormulaeStatus.swift
|
||||
// BusyStatus.swift
|
||||
// PHP Monitor
|
||||
//
|
||||
// Created by Nico Verbruggen on 02/05/2023.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class PhpFormulaeStatus: ObservableObject {
|
||||
class BusyStatus: ObservableObject {
|
||||
@Published var busy: Bool
|
||||
@Published var title: String
|
||||
@Published var description: String
|
||||
@ -18,4 +18,12 @@ class PhpFormulaeStatus: ObservableObject {
|
||||
self.title = title
|
||||
self.description = description
|
||||
}
|
||||
|
||||
public static func notBusy() -> BusyStatus {
|
||||
return BusyStatus(busy: false, title: "", description: "")
|
||||
}
|
||||
|
||||
public static func busy() -> BusyStatus {
|
||||
return BusyStatus(busy: false, title: "", description: "")
|
||||
}
|
||||
}
|
@ -63,8 +63,8 @@ public struct TestableConfiguration: Codable {
|
||||
: .fake(.binary),
|
||||
"/opt/homebrew/Cellar/php/\(version.long)/bin/php-config"
|
||||
: .fake(.binary),
|
||||
// "/opt/homebrew/etc/php/\(version.short)/php-fpm.d/www.conf"
|
||||
// : .fake(.text),
|
||||
"/opt/homebrew/etc/php/\(version.short)/php-fpm.d/www.conf"
|
||||
: .fake(.text),
|
||||
"/opt/homebrew/etc/php/\(version.short)/php-fpm.d/valet-fpm.conf"
|
||||
: .fake(.text),
|
||||
"/opt/homebrew/etc/php/\(version.short)/php.ini"
|
||||
@ -73,12 +73,26 @@ public struct TestableConfiguration: Codable {
|
||||
: .fake(.text)
|
||||
]) { (_, new) in new }
|
||||
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"]
|
||||
= version.long
|
||||
// PHP configuration files
|
||||
self.shellOutput["/opt/homebrew/opt/php@\(version.short)/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||
|
||||
// PHP Homebrew operations
|
||||
self.shellOutput["/opt/homebrew/bin/brew unlink php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services start php@\(version.short)"] = .delayed(0.2, "OK")
|
||||
self.shellOutput["/opt/homebrew/bin/brew link php@\(version.short) --overwrite --force"] = .delayed(0.2, "OK")
|
||||
|
||||
// PHP version output
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php-config --version"] = version.long
|
||||
self.commandOutput["/opt/homebrew/opt/php@\(version.short)/bin/php -v"] = "OK"
|
||||
|
||||
if primary {
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"]
|
||||
= .instant("php")
|
||||
// Files expected to be present for currently linked PHP version
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php"] =
|
||||
.instant("php")
|
||||
self.shellOutput["/opt/homebrew/bin/php --ini | grep -E -o '(/[^ ]+\\.ini)'"] =
|
||||
.instant("/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini")
|
||||
self.filesystem["/opt/homebrew/opt/php"]
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.long)")
|
||||
self.filesystem["/opt/homebrew/opt/php/bin/php"]
|
||||
@ -89,12 +103,8 @@ public struct TestableConfiguration: Codable {
|
||||
= .fake(.symlink, "/opt/homebrew/Cellar/php/\(version.short)/bin/php-config")
|
||||
self.commandOutput["/opt/homebrew/bin/php-config --version"]
|
||||
= version.long
|
||||
self.commandOutput["/opt/homebrew/bin/php -r echo php_ini_scanned_files();"] =
|
||||
"""
|
||||
/opt/homebrew/etc/php/\(version.short)/conf.d/php-memory-limits.ini,
|
||||
"""
|
||||
} else {
|
||||
self.shellOutput["sudo /opt/homebrew/bin/brew services stop php@\(version.short)"] = .instant("")
|
||||
// Output expected to be present for non-linked PHP versions
|
||||
self.shellOutput["ls /opt/homebrew/opt | grep php@"] =
|
||||
BatchFakeShellOutput.instant(
|
||||
self.secondaryPhpVersions
|
||||
@ -103,6 +113,7 @@ public struct TestableConfiguration: Codable {
|
||||
)
|
||||
}
|
||||
}
|
||||
// swiftlint:enable function_body_length
|
||||
|
||||
// MARK: Interactions
|
||||
|
||||
|
@ -18,11 +18,11 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
self.files = files
|
||||
|
||||
// Ensure that each of the ~ characters are replaced with the home directory path
|
||||
for key in self.files.keys where key.contains("~") {
|
||||
self.files.renameKey(
|
||||
fromKey: key,
|
||||
toKey: key.replacingOccurrences(of: "~", with: self.homeDirectory)
|
||||
)
|
||||
accessQueue.sync {
|
||||
for (key, value) in files {
|
||||
let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key
|
||||
self.files[adjustedKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that intermediate directories are created
|
||||
@ -46,38 +46,49 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
*/
|
||||
private(set) var homeDirectory = "/Users/fake"
|
||||
|
||||
/**
|
||||
Serial dispatch queue for ensuring thread-safe access to the `files` dictionary.
|
||||
*/
|
||||
private let accessQueue = DispatchQueue(label: "com.testablefilesystem.accessQueue")
|
||||
|
||||
// MARK: - Basics
|
||||
|
||||
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
try accessQueue.sync {
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
}
|
||||
|
||||
self.createIntermediateDirectories(path)
|
||||
|
||||
self.files[path] = .fake(.directory)
|
||||
}
|
||||
|
||||
self.createIntermediateDirectories(path)
|
||||
|
||||
self.files[path] = .fake(.directory)
|
||||
}
|
||||
|
||||
func writeAtomicallyToFile(_ path: String, content: String) throws {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
}
|
||||
try accessQueue.sync {
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
}
|
||||
|
||||
self.files[path] = .fake(.text, content)
|
||||
self.files[path] = .fake(.text, content)
|
||||
}
|
||||
}
|
||||
|
||||
func getStringFromFile(_ path: String) throws -> String {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
return try accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
|
||||
return file.content ?? ""
|
||||
return file.content ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
|
||||
@ -88,32 +99,36 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
seek = "\(seek)/"
|
||||
}
|
||||
|
||||
return self.files.keys
|
||||
.filter { $0.hasPrefix(seek) }
|
||||
.map { $0.replacingOccurrences(of: seek, with: "") }
|
||||
.filter { !$0.contains("/") }
|
||||
return accessQueue.sync {
|
||||
self.files.keys
|
||||
.filter { $0.hasPrefix(seek) }
|
||||
.map { $0.replacingOccurrences(of: seek, with: "") }
|
||||
.filter { !$0.contains("/") }
|
||||
}
|
||||
}
|
||||
|
||||
func getDestinationOfSymlink(_ path: String) throws -> String {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
return try accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
|
||||
if file.type != .symlink {
|
||||
throw TestableFileSystemError.notSymlink
|
||||
}
|
||||
if file.type != .symlink {
|
||||
throw TestableFileSystemError.notSymlink
|
||||
}
|
||||
|
||||
guard let pathToSymlink = file.content else {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
guard let pathToSymlink = file.content else {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
|
||||
if !files.keys.contains(pathToSymlink) {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
if !files.keys.contains(pathToSymlink) {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
|
||||
return pathToSymlink
|
||||
return pathToSymlink
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Move & Delete Files
|
||||
@ -122,27 +137,31 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
let newPath = newPath.replacingTildeWithHomeDirectory
|
||||
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.renameKey(
|
||||
fromKey: key,
|
||||
toKey: key.replacingOccurrences(of: path, with: newPath)
|
||||
)
|
||||
accessQueue.sync {
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.renameKey(
|
||||
fromKey: key,
|
||||
toKey: key.replacingOccurrences(of: path, with: newPath)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files.renameKey(fromKey: path, toKey: newPath)
|
||||
self.files.renameKey(fromKey: path, toKey: newPath)
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ path: String) throws {
|
||||
// Remove recursively
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.removeValue(forKey: key)
|
||||
accessQueue.sync {
|
||||
// Remove recursively
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files.removeValue(forKey: path)
|
||||
self.files.removeValue(forKey: path)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Attributes
|
||||
@ -150,11 +169,13 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
func makeExecutable(_ path: String) throws {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
try accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
|
||||
file.type = .binary
|
||||
file.type = .binary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
@ -162,93 +183,107 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
func isExecutableFile(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.type == .binary
|
||||
return file.type == .binary
|
||||
}
|
||||
}
|
||||
|
||||
func isWriteableFile(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return !file.readOnly
|
||||
return !file.readOnly
|
||||
}
|
||||
}
|
||||
|
||||
func anyExists(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
return files.keys.contains(path)
|
||||
return accessQueue.sync {
|
||||
files.keys.contains(path)
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return [.binary, .symlink, .text].contains(file.type)
|
||||
return [.binary, .symlink, .text].contains(file.type)
|
||||
}
|
||||
}
|
||||
|
||||
func directoryExists(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return [.directory].contains(file.type)
|
||||
return [.directory].contains(file.type)
|
||||
}
|
||||
}
|
||||
|
||||
func isSymlink(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.type == .symlink
|
||||
return file.type == .symlink
|
||||
}
|
||||
}
|
||||
|
||||
func isDirectory(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.type == .directory
|
||||
return file.type == .directory
|
||||
}
|
||||
}
|
||||
|
||||
public func printContents() {
|
||||
for key in self.files.keys.sorted() {
|
||||
print("\(key) -> \(self.files[key]!.type)")
|
||||
accessQueue.sync {
|
||||
for key in self.files.keys.sorted() {
|
||||
print("\(key) -> \(self.files[key]!.type)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createIntermediateDirectories(_ path: String) {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
let items = path.components(separatedBy: "/")
|
||||
|
||||
var preceding = ""
|
||||
|
||||
var directoriesToCreate: [String] = []
|
||||
|
||||
for item in items {
|
||||
let key = preceding == "/"
|
||||
? "/\(item)"
|
||||
: "\(preceding)/\(item)"
|
||||
|
||||
if !self.files.keys.contains(key) {
|
||||
self.files[key] = .fake(.directory)
|
||||
}
|
||||
|
||||
let key = preceding == "/" ? "/\(item)" : "\(preceding)/\(item)"
|
||||
directoriesToCreate.append(key)
|
||||
preceding = key
|
||||
}
|
||||
|
||||
for key in directoriesToCreate where !self.files.keys.contains(key) {
|
||||
self.files[key] = .fake(.directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,17 @@ public class TestableShell: ShellProtocol {
|
||||
|
||||
var expectations: [String: BatchFakeShellOutput] = [:]
|
||||
|
||||
func sync(_ command: String) -> ShellOutput {
|
||||
// This assertion will only fire during test builds
|
||||
assert(expectations.keys.contains(command), "No response declared for command: \(command)")
|
||||
|
||||
guard let expectation = expectations[command] else {
|
||||
return .err("No Expected Output")
|
||||
}
|
||||
|
||||
return expectation.syncOutput()
|
||||
}
|
||||
|
||||
func quiet(_ command: String) async {
|
||||
_ = try! await self.attach(command, didReceiveOutput: { _, _ in }, withTimeout: 60)
|
||||
}
|
||||
@ -112,6 +123,29 @@ struct BatchFakeShellOutput: Codable {
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
Outputs the fake shell output as expected, but does this synchronously.
|
||||
*/
|
||||
public func syncOutput(
|
||||
ignoreDelay: Bool = false
|
||||
) -> ShellOutput {
|
||||
let output = ShellOutput.empty()
|
||||
|
||||
for item in items {
|
||||
if !ignoreDelay {
|
||||
Thread.sleep(forTimeInterval: item.delay)
|
||||
}
|
||||
|
||||
if item.stream == .stdErr {
|
||||
output.err += item.output
|
||||
} else if item.stream == .stdOut {
|
||||
output.out += item.output
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
For testing purposes (and speed) we may omit the delay, regardless of its timespan.
|
||||
*/
|
||||
|
@ -16,10 +16,19 @@
|
||||
<p><b>Do you enjoy using the app? Is it helping you save time?</b> Leave a <a href="https://phpmon.app/github">star on GitHub</a>!</p>
|
||||
<p><b>Having issues?</b> Consult the <a href="https://phpmon.app/faq">FAQ</a> section, I did my best to ensure everything is documented.</p>
|
||||
<p><b>Want to support further development of PHP Monitor?</b> You can <a href="https://phpmon.app/sponsor">financially support</a> the continued development of this app.</p>
|
||||
<p><b>Get the latest on Twitter or Mastodon.</b> Give me a <a href="https://twitter.com/nicoverbruggen">follow on Twitter</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||
<p><b>Get the latest on Bluesky or Mastodon.</b> Give me a <a href="https://bsky.app/profile/nicoverbruggen.be">follow on Bluesky</a> or <a href="https://phpc.social/@nicoverbruggen">Mastodon</a> to learn about what's brewing and when new updates drop.</p>
|
||||
<p><b>Special thanks</b> to all current and past <a href="https://github.com/sponsors/nicoverbruggen#sponsors"><b>sponsors</b></a> of PHP Monitor, who have helped to make further development of the app possible.</p>
|
||||
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @caseyalee, @cgreuling, @cjcox17, @Diewy, @drfraker, @driftingly, @duellsy, @edalzell, @EYOND, @faithfm, @frankmichel, @gwleuverink, @hopkins385, @intrepidws, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @Laravel-Backpack, @leganz, @martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rderimay, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @SRWieZ, @stefanbauer, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(Some names have been omitted due to their sponsorships being private. Thank you all!)
|
||||
<p><b>Made possible by these GitHub Sponsors</b>: @abdusfauzi, @abicons, @adibnoh, @adrolli, @andresayej, @andyunleashed, @anzacorp, @argirisp, @ash-jc-allen, @AshPowell, @aurawindsurfing, @awsmug, @barrycarton, @BertvanHoekelen, @calebporzio, @casenxu, @caseyalee, @cgreuling, @cjcox17, @clescuyer, @codelinde, @designhammer, @Diewy, @drfraker, @driftingly, @duellsy, @e9li, @edalzell, @EYOND, @faithfm, @frankmichel, @gekich, @gpluess, @gwleuverink, @hopkins385, @incon, @intrepidws, @israaraujo, @jacksleight, @JacobBennett, @jasonvarga, @jeromegamez, @jimmyaldape, @jimmysawczuk, @joetannenbaum, @jolora, @jorisnoo, @joshuablum, @jpeinelt, @jreviews, @JustSteveKing, @Kajvdh, @KFoobar, @kholisabdullah, @Laravel-Backpack, @leganz, @lucianvacaroiu,@martinleveille, @mathiasonea, @matthewmnewman, @mcastillo1030, @megabubbletea, @megabubbleteam, @mennen-online, @mike-healy, @mostafakram, @mpociot, @MrMicky-FR, @MrMooky, @murdercode, @nckrtl, @nhedger, @ninjaparade, @ozanuzer, @pepatel, @philbraun, @pickuse2013, @pk-informatics, @Plytas, @rastitkac, @rderimay, @renecum, @richardhulbert, @richardtape, @rickyjohnston, @rico, @RobertBoes, @runofthemill, @SahinU88, @sdebacker, @sdevore, @shadracnicholas, @simonhamp, @slaFFik, @spatie, @SRWieZ, @stefanbauer, @stefanzweifel, @StriveMedia, @swilla, @Tailcode-Studio, @theutz, @ThomasEnssner, @tillkruss, @timothyrowan, @ttnppedr, @vincent-tarrit, @vintagesucks, @WheresMarco, @xPand4B, @xuandung38, @yeslandi89, @zackkatz, @zacksmash, @zaherg.<br/>(This is a historical list of sponsors, not current sponsors. Some names have been omitted due to their sponsorships being private. Thank you all!)</p>
|
||||
<p><b>Localization credits:</b></br>
|
||||
‐ English, Dutch</b> by @nicoverbruggen</br>
|
||||
‐ Vietnamese</b> by @xuandung38</br>
|
||||
‐ German</b> by @dsturm</br>
|
||||
‐ Portuguese</b> by @joseborges</br>
|
||||
‐ French</b> by @nhedger, @tplesnar</br>
|
||||
‐ Chinese</b> by @guanguans</br>
|
||||
</br>
|
||||
Other languages are considered experimental, and were generated via a local LLM. If you have feedback or concerns, please don't hesitate to get in touch.
|
||||
</p>
|
||||
<br/>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -83,9 +83,15 @@ class App {
|
||||
/** The window controller of the PHP version manager window. */
|
||||
var phpVersionManagerWindowController: PhpVersionManagerWindowController?
|
||||
|
||||
/** The window controller of the PHP extension manager window. */
|
||||
var phpExtensionManagerWindowController: PhpExtensionManagerWindowController?
|
||||
|
||||
/** List of detected (installed) applications that PHP Monitor can work with. */
|
||||
var detectedApplications: [Application] = []
|
||||
|
||||
/** Favorites storage, which keeps track of favorited domains. */
|
||||
var favorites = Favorites.shared
|
||||
|
||||
/** The warning manager, responsible for keeping track of warnings. */
|
||||
var warnings = WarningManager.shared
|
||||
|
||||
|
@ -23,12 +23,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
*/
|
||||
let state: App
|
||||
|
||||
/**
|
||||
The MainMenu singleton is responsible for rendering the
|
||||
menu bar item and its menu, as well as its actions.
|
||||
*/
|
||||
let menu: MainMenu
|
||||
|
||||
/**
|
||||
The paths singleton that determines where Homebrew is installed,
|
||||
and where to look for binaries.
|
||||
@ -96,7 +90,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
|
||||
self.state = App.shared
|
||||
self.menu = MainMenu.shared
|
||||
self.paths = Paths.shared
|
||||
self.valet = Valet.shared
|
||||
self.brew = Brew.shared
|
||||
@ -109,6 +102,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
|
||||
static func initializeTestingProfile(_ path: String) {
|
||||
Log.info("The configuration with path `\(path)` is being requested...")
|
||||
// Clear for PHP Guard
|
||||
Stats.clearCurrentGlobalPhpVersion()
|
||||
// Load the configuration file
|
||||
TestableConfiguration.loadFrom(path: path).apply()
|
||||
}
|
||||
|
||||
@ -129,7 +125,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
|
||||
setupNotifications()
|
||||
|
||||
Task { // Make sure the menu performs its initial checks
|
||||
await menu.startup()
|
||||
await MainMenu.shared.startup()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import NVAlert
|
||||
|
||||
class AppUpdater {
|
||||
var caskFile: CaskFile!
|
||||
@ -72,7 +73,7 @@ class AppUpdater {
|
||||
: "brew upgrade phpmon"
|
||||
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
NVAlert().withInformation(
|
||||
title: "updater.alerts.newer_version_available.title"
|
||||
.localized(latestVersionOnline.humanReadable),
|
||||
subtitle: "updater.alerts.newer_version_available.subtitle"
|
||||
@ -112,7 +113,7 @@ class AppUpdater {
|
||||
|
||||
public func presentNoNewerVersionAvailableAlert() {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
NVAlert().withInformation(
|
||||
title: "updater.alerts.is_latest_version.title".localized,
|
||||
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
|
||||
description: ""
|
||||
@ -124,7 +125,7 @@ class AppUpdater {
|
||||
|
||||
public func presentCouldNotRetrieveUpdate() {
|
||||
Task { @MainActor in
|
||||
BetterAlert().withInformation(
|
||||
NVAlert().withInformation(
|
||||
title: "updater.alerts.cannot_check_for_update.title".localized,
|
||||
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
|
||||
description: "updater.alerts.cannot_check_for_update.description".localized(
|
||||
|