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

Compare commits

..

15 Commits

Author SHA1 Message Date
9da3772212 🚀 Version 5.6.3 2022-10-22 17:34:53 +02:00
e62b03d070 👌 Style fix 2022-10-22 16:43:05 +02:00
9a11d2efed 🔧 Bump build 2022-10-22 16:42:26 +02:00
b134e62328 🐛 Handle empty output for brew info 2022-10-22 16:41:54 +02:00
5c69133c42 👌 Add brew tap homebrew/services instruction
This now recommends the appropriate solution for #208.
2022-10-20 20:44:44 +02:00
f4448e0640 🔧 Bump version number 2022-10-10 21:49:51 +02:00
7fd30d7c54 Add preference to disable TLD alert (#206) 2022-10-10 21:49:43 +02:00
2c57dea97f 🔀 Merge branch 'main' into dev/5.6 2022-10-09 22:00:36 +02:00
a77fa5557a 📝 Update README for Login Items on Ventura 2022-10-09 21:56:13 +02:00
45704fc736 🚀 Version 5.6.2 2022-10-02 13:28:58 +02:00
f28354e634 🐛 Use valet secure sitename (#197) 2022-10-02 13:28:01 +02:00
8055a32bde 🐛 Fix ComposerWindow deinit not firing 2022-09-29 18:50:40 +02:00
5b3054326e 🔧 Bump version number 2022-09-28 18:24:26 +02:00
e7f3c7e59c 🐛 Fix an issue with missing separator item 2022-09-28 18:24:01 +02:00
a407515534 🚀 Version 5.6.1 2022-09-24 12:56:14 +02:00
165 changed files with 2361 additions and 7544 deletions

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -79,71 +79,28 @@ If you're still having issues, here's a few common questions & answers, as well
<details>
<summary><strong>Which versions of PHP are supported?</strong></summary>
The following versions of PHP are officially supported:
<ul>
<li>PHP 5.6 (only if you are running Valet 2)</li>
<li>PHP 7.0</li>
<li>PHP 7.1</li>
<li>PHP 7.2</li>
<li>PHP 7.3</li>
<li>PHP 7.4</li>
<li>PHP 8.0</li>
<li>PHP 8.1</li>
<li>PHP 8.2</li>
</ul>
The following versions have some support via backport and/or dev version:
<ul>
<li>PHP 5.6 (Valet 2 only)</li>
<li>PHP 7.0 (Valet 2 and 3 only)</li>
<li>PHP 7.1 (Valet 2 and 3 only)</li>
<li>PHP 7.2 (Valet 2 and 3 only)</li>
<li>PHP 7.3 (Valet 2 and 3 only)</li>
</ul>
Additionally, the following dev version is also available:
<ul>
<li>PHP 8.3-dev (experimental)</li>
<li>PHP 8.2 (experimental)</li>
</ul>
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.
Backports are available via [this tap](https://github.com/shivammathur/homebrew-php). For more information about those backports, please see the next FAQ entry.
For maximum compatibility with older PHP versions, you may wish to keep using Valet 2 or 3.
</details>
<details>
<summary><strong>How do I install additional versions of PHP, including legacy versions?</strong></summary>
Assuming you have installed the `php` formula, the latest stable version of PHP is installed. At the time of writing, this is PHP 8.2.
You can install other supported versions of PHP out of the box, so `php@8.0` and `php@8.1` at the time of writing.
If you wish to install older (officially unsupported) versions of PHP for local use, you can do so by using [Shivam Mathur's tap](https://github.com/shivammathur/homebrew-php):
```sh
brew tap shivammathur/php
```
You may find that this tap is already in use: if you've used Valet before, it automatically uses this tap for legacy versions of PHP.
You can then install those older versions:
```sh
brew install php@7.0
brew install php@7.1
...
```
**Always make sure to restart PHP Monitor after installing or upgrading PHP versions!**
> *Note*: Using this tap may cause [temporary alias conflicts](https://github.com/nicoverbruggen/phpmon/issues/54#issuecomment-979789724) while the core tap alias and the tap's alias refer to a different version of PHP, but this is generally speaking a minor inconvenience, since this normally only applies when a new PHP version releases.
</details>
<details>
<summary><strong>I want PHP Monitor to start up when I boot my Mac!</strong></summary>
You can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
On macOS Ventura, you can accomplish this by going to **System Settings > General > Login Items** and adding PHP Monitor.app to the list **Open at Login**. You can do this with any application, by the way.
On older versions of macOS, you can do this by dragging *PHP Monitor.app* into the **Login Items** section in **System Preferences > Users & Groups** for your account.
Super convenient!
</details>

View File

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

Binary file not shown.

View File

@ -1,6 +1,6 @@
//
// CommandTest.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
@ -10,11 +10,10 @@ import XCTest
class CommandTest: XCTestCase {
func test_determine_php_version() {
func testDeterminePhpVersion() {
let version = Command.execute(
path: Paths.php,
arguments: ["-v"],
trimNewlines: false
arguments: ["-v"]
)
XCTAssert(version.contains("(cli)"))

22
phpmon-tests/Info.plist Normal file
View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View File

@ -1,6 +1,6 @@
//
// BrewJsonParserTest.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
@ -17,7 +17,7 @@ class HomebrewPackageTest: XCTestCase {
.url(forResource: "brew-formula", withExtension: "json")!
}
func test_can_load_extension_json() throws {
func testCanLoadExtensionJson() throws {
let json = try! String(contentsOf: Self.jsonBrewFile, encoding: .utf8)
let package = try! JSONDecoder().decode(
[HomebrewPackage].self, from: json.data(using: .utf8)!
@ -25,9 +25,9 @@ class HomebrewPackageTest: XCTestCase {
XCTAssertEqual(package.name, "php")
XCTAssertEqual(package.full_name, "php")
XCTAssertEqual(package.aliases.first!, "php@8.1")
XCTAssertEqual(package.aliases.first!, "php@8.0")
XCTAssertEqual(package.installed.contains(where: { installed in
installed.version.starts(with: "8.1")
installed.version.starts(with: "8.0")
}), true)
}
@ -36,7 +36,7 @@ class HomebrewPackageTest: XCTestCase {
.url(forResource: "brew-services", withExtension: "json")!
}
func test_can_parse_services_json() throws {
func testCanParseServicesJson() throws {
let json = try! String(contentsOf: Self.jsonBrewServicesFile, encoding: .utf8)
let services = try! JSONDecoder().decode(
[HomebrewService].self, from: json.data(using: .utf8)!
@ -47,21 +47,19 @@ class HomebrewPackageTest: XCTestCase {
XCTAssertEqual(services.first?.service_name, "homebrew.mxcl.dnsmasq")
}
/*
// - MARK: LIVE TESTS
/// This test requires that you have a valid Homebrew installation set up,
/// and requires the Valet services to be installed: php, nginx and dnsmasq.
/// If this test fails, there is an issue with your Homebrew installation
/// or the JSON API of the Homebrew output may have changed.
func test_can_parse_services_json_from_cli_output() async throws {
ActiveShell.useSystem()
func testCanParseServicesJsonFromCliOutput() throws {
let services = try! JSONDecoder().decode(
[HomebrewService].self,
from: await Shell.pipe(
"sudo \(Paths.brew) services info --all --json"
).out.data(using: .utf8)!
from: Shell.pipe(
"sudo \(Paths.brew) services info --all --json",
requiresPath: true
).data(using: .utf8)!
).filter({ service in
return ["php", "nginx", "dnsmasq"].contains(service.name)
})
@ -76,15 +74,12 @@ class HomebrewPackageTest: XCTestCase {
/// and requires the `php` formula to be installed.
/// If this test fails, there is an issue with your Homebrew installation
/// or the JSON API of the Homebrew output may have changed.
func test_can_load_extension_json_from_cli_output() async throws {
ActiveShell.useSystem()
func testCanLoadExtensionJsonFromCliOutput() throws {
let package = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: await Shell.pipe("\(Paths.brew) info php --json").out.data(using: .utf8)!
from: Shell.pipe("\(Paths.brew) info php --json", requiresPath: true).data(using: .utf8)!
).first!
XCTAssertTrue(package.name == "php")
}
*/
}

View File

@ -1,6 +1,6 @@
//
// NginxConfigurationTest.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
@ -34,7 +34,7 @@ class NginxConfigurationTest: XCTestCase {
// MARK: - Tests
func test_can_determine_site_name_and_tld() throws {
func testCanDetermineSiteNameAndTld() throws {
XCTAssertEqual(
"nginx-site",
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain
@ -45,7 +45,7 @@ class NginxConfigurationTest: XCTestCase {
)
}
func test_can_determine_isolation() throws {
func testCanDetermineIsolation() throws {
XCTAssertNil(
NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion
)
@ -56,7 +56,7 @@ class NginxConfigurationTest: XCTestCase {
)
}
func test_can_determine_proxy() throws {
func testCanDetermineProxy() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.proxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: proxy.valet.conf"))
XCTAssertEqual("http://127.0.0.1:90", proxied.proxy)
@ -66,13 +66,13 @@ class NginxConfigurationTest: XCTestCase {
XCTAssertEqual(nil, normal.proxy)
}
func test_can_determine_secured_proxy() throws {
func testCanDetermineSecuredProxy() throws {
let proxied = NginxConfigurationFile.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 test_can_determine_proxy_with_custom_tld() throws {
func testCanDetermineProxyWithCustomTld() throws {
let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)!
XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf"))
XCTAssertEqual("http://localhost:8080", proxied.proxy)

View File

@ -14,7 +14,7 @@ class PhpConfigurationTest: XCTestCase {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func test_can_load_extension() throws {
func testCanLoadExtension() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile)
@ -22,7 +22,7 @@ class PhpConfigurationTest: XCTestCase {
XCTAssertGreaterThan(iniFile.extensions.count, 0)
}
func test_can_check_key_existence() throws {
func testCanCheckKeyExistence() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertTrue(iniFile.has(key: "error_reporting"))
@ -30,7 +30,7 @@ class PhpConfigurationTest: XCTestCase {
XCTAssertFalse(iniFile.has(key: "my_unknown_key"))
}
func test_can_check_key_value() throws {
func testCanCheckKeyValue() throws {
let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)!
XCTAssertNotNil(iniFile.get(for: "error_reporting"))
@ -40,7 +40,7 @@ class PhpConfigurationTest: XCTestCase {
XCTAssert(iniFile.get(for: "display_errors") == "On")
}
func test_can_customize_configuration_value() throws {
func testCanCustomizeConfigurationValue() throws {
let destination = Utility
.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!

View File

@ -1,6 +1,6 @@
//
// ExtensionParserTest.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 13/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
@ -14,13 +14,13 @@ class PhpExtensionTest: XCTestCase {
return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")!
}
func test_can_load_extension() throws {
func testCanLoadExtension() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
XCTAssertGreaterThan(extensions.count, 0)
}
func test_extension_name_is_correct() throws {
func testExtensionNameIsCorrect() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
let extensionNames = extensions.map { (ext) -> String in
@ -39,7 +39,7 @@ class PhpExtensionTest: XCTestCase {
XCTAssertFalse(extensionNames.contains("nice"))
}
func test_extension_status_is_correct() throws {
func testExtensionStatusIsCorrect() throws {
let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path)
// xdebug should be enabled
@ -49,7 +49,7 @@ class PhpExtensionTest: XCTestCase {
XCTAssertEqual(extensions[1].enabled, false)
}
func test_toggle_works_as_expected() async throws {
func testToggleWorksAsExpected() throws {
let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")!
let extensions = PhpExtension.from(filePath: destination.path)
XCTAssertEqual(extensions.count, 6)
@ -58,7 +58,7 @@ class PhpExtensionTest: XCTestCase {
let xdebug = extensions.first!
XCTAssertTrue(xdebug.name == "xdebug")
XCTAssertEqual(xdebug.enabled, true)
await xdebug.toggle()
xdebug.toggle()
XCTAssertEqual(xdebug.enabled, false)
// Check if the file contains the appropriate data

View File

@ -1,6 +1,6 @@
//
// ValetConfigParserTest.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 29/11/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
@ -17,7 +17,7 @@ class ValetConfigurationTest: XCTestCase {
)!
}
func test_can_load_config_file() throws {
func testCanLoadConfigFile() throws {
let json = try? String(
contentsOf: Self.jsonConfigFileUrl,
encoding: .utf8

View File

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

View File

@ -1,6 +1,6 @@
//
// Utility.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 14/02/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.

View File

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

View File

@ -10,11 +10,11 @@ import XCTest
class AppVersionTest: XCTestCase {
func test_can_retrieve_internal_app_version() {
func testCanRetrieveInternalAppVersion() {
XCTAssertNotNil(AppVersion.fromCurrentVersion())
}
func test_can_parse_normal_version_string() {
func testCanParseNormalVersionString() {
let version = AppVersion.from("1.0.0")
XCTAssertNotNil(version)
@ -23,7 +23,7 @@ class AppVersionTest: XCTestCase {
XCTAssertEqual(nil, version?.suffix)
}
func test_can_parse_cask_version_string() {
func testCanParseCaskVersionString() {
let version = AppVersion.from("1.0.0_600")
XCTAssertNotNil(version)
@ -32,7 +32,7 @@ class AppVersionTest: XCTestCase {
XCTAssertEqual(nil, version?.suffix)
}
func test_can_parse_dev_version_string_without_build_number() {
func testCanParseDevVersionStringWithoutBuildNumber() {
let version = AppVersion.from("1.0.0-dev")
XCTAssertNotNil(version)
@ -41,7 +41,7 @@ class AppVersionTest: XCTestCase {
XCTAssertEqual("dev", version?.suffix)
}
func test_can_parse_dev_version_string_with_build_number() {
func testCanParseDevVersionStringWithBuildNumber() {
let version = AppVersion.from("1.0.0-dev,870")
XCTAssertNotNil(version)
@ -50,7 +50,7 @@ class AppVersionTest: XCTestCase {
XCTAssertEqual("dev", version?.suffix)
}
func test_can_parse_underscores_as_build_separator() {
func testCanParseUnderscoresAsBuildSeparatorToo() {
let version = AppVersion.from("1.0.0-dev_870")
XCTAssertNotNil(version)

View File

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

View File

@ -11,40 +11,40 @@ import XCTest
// swiftlint:disable type_body_length
class PhpVersionNumberTest: XCTestCase {
func test_can_deconstruct_php_version() throws {
func testCanDeconstructPhpVersion() throws {
XCTAssertEqual(
try! VersionNumber.parse("PHP 8.2.0-dev"),
VersionNumber(major: 8, minor: 2, patch: 0)
try! PhpVersionNumber.parse("PHP 8.2.0-dev"),
PhpVersionNumber(major: 8, minor: 2, patch: 0)
)
XCTAssertEqual(
try! VersionNumber.parse("PHP 8.1.0RC5-dev"),
VersionNumber(major: 8, minor: 1, patch: 0)
try! PhpVersionNumber.parse("PHP 8.1.0RC5-dev"),
PhpVersionNumber(major: 8, minor: 1, patch: 0)
)
XCTAssertEqual(
try! VersionNumber.parse("8.0.11"),
VersionNumber(major: 8, minor: 0, patch: 11)
try! PhpVersionNumber.parse("8.0.11"),
PhpVersionNumber(major: 8, minor: 0, patch: 11)
)
XCTAssertEqual(
try! VersionNumber.parse("7.4.2"),
VersionNumber(major: 7, minor: 4, patch: 2)
try! PhpVersionNumber.parse("7.4.2"),
PhpVersionNumber(major: 7, minor: 4, patch: 2)
)
XCTAssertEqual(
try! VersionNumber.parse("7.4"),
VersionNumber(major: 7, minor: 4, patch: nil)
try! PhpVersionNumber.parse("7.4"),
PhpVersionNumber(major: 7, minor: 4, patch: nil)
)
XCTAssertEqual(
VersionNumber.make(from: "7"),
PhpVersionNumber.make(from: "7"),
nil
)
}
func test_php_version_number_parse() throws {
XCTAssertThrowsError(try VersionNumber.parse("OOF")) { error in
func testPhpVersionNumberParse() throws {
XCTAssertThrowsError(try PhpVersionNumber.parse("OOF")) { error in
XCTAssertTrue(error is VersionParseError)
}
}
func test_can_check_fixed_constraints() throws {
func testCanCheckFixedConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -78,7 +78,7 @@ class PhpVersionNumberTest: XCTestCase {
)
}
func test_can_check_caret_constraints() throws {
func testCanCheckCaretConstraints() throws {
// 1. Imprecise checks
XCTAssertEqual(
PhpVersionNumberCollection
@ -138,7 +138,7 @@ class PhpVersionNumberTest: XCTestCase {
)
}
func test_can_check_tilde_constraints() throws {
func testCanCheckTildeConstraints() throws {
// 1. Imprecise checks
XCTAssertEqual(
PhpVersionNumberCollection
@ -207,7 +207,7 @@ class PhpVersionNumberTest: XCTestCase {
)
}
func test_can_check_greater_than_or_equal_constraints() throws {
func testCanCheckGreaterThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -243,7 +243,7 @@ class PhpVersionNumberTest: XCTestCase {
)
}
func test_can_check_greater_than_constraints() throws {
func testCanCheckGreaterThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -289,7 +289,7 @@ class PhpVersionNumberTest: XCTestCase {
)
}
func test_can_check_less_than_or_equal_constraints() throws {
func testCanCheckLessThanOrEqualConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])
@ -325,7 +325,7 @@ class PhpVersionNumberTest: XCTestCase {
)
}
func test_can_check_less_than_constraints() throws {
func testCanCheckLessThanConstraints() throws {
XCTAssertEqual(
PhpVersionNumberCollection
.make(from: ["7.4", "7.3", "7.2", "7.1", "7.0"])

View File

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

View File

@ -1,6 +1,6 @@
//
// VersionExtractorTest.swift
// PHP Monitor
// phpmon-tests
//
// Created by Nico Verbruggen on 16/12/2021.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
@ -10,12 +10,12 @@ import XCTest
class VersionExtractorTest: XCTestCase {
func test_extract_version() {
func testExtractVersion() {
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.17.1"), "2.17.1")
XCTAssertEqual(VersionExtractor.from("Laravel Valet 2.0"), "2.0")
}
func test_version_comparison() {
func testVersionComparison() {
XCTAssertEqual("2.0".versionCompare("2.1"), .orderedAscending)
XCTAssertEqual("2.1".versionCompare("2.0"), .orderedDescending)
XCTAssertEqual("2.0".versionCompare("2.0"), .orderedSame)

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 575 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

View File

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

View File

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

View File

@ -12,37 +12,36 @@ class Actions {
// MARK: - Services
public static func restartPhpFpm() async {
await brew("services restart \(Homebrew.Formulae.php.name)", sudo: Homebrew.Formulae.php.elevated)
public static func restartPhpFpm() {
brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true)
}
public static func restartNginx() async {
await brew("services restart \(Homebrew.Formulae.nginx.name)", sudo: Homebrew.Formulae.nginx.elevated)
public static func restartNginx() {
brew("services restart nginx", sudo: true)
}
public static func restartDnsMasq() async {
await brew("services restart \(Homebrew.Formulae.dnsmasq.name)", sudo: Homebrew.Formulae.dnsmasq.elevated)
public static func restartDnsMasq() {
brew("services restart dnsmasq", sudo: true)
}
public static func stopValetServices() async {
await brew("services stop \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
await brew("services stop \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
await brew("services stop \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
public static func stopValetServices() {
brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true)
brew("services stop nginx", sudo: true)
brew("services stop dnsmasq", sudo: true)
}
public static func fixHomebrewPermissions() throws {
var servicesCommands = [
"\(Paths.brew) services stop \(Homebrew.Formulae.nginx)",
"\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)"
"\(Paths.brew) services stop nginx",
"\(Paths.brew) services stop dnsmasq"
]
var cellarCommands = [
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)"
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx",
"chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq"
]
PhpEnv.shared.availablePhpVersions.forEach { version in
let formula = version == PhpEnv.brewPhpAlias
let formula = version == PhpEnv.brewPhpVersion
? "php"
: "php@\(version)"
servicesCommands.append("\(Paths.brew) services stop \(formula)")
@ -65,6 +64,29 @@ class Actions {
}
}
// MARK: - Third Party Services
public static func stopService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services stop \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
public static func startService(name: String, completion: @escaping () -> Void) {
DispatchQueue.global(qos: .userInitiated).async {
brew("services start \(name)", sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name })
ServicesManager.loadHomebrewServices(completed: {
DispatchQueue.main.async {
completion()
}
})
}
}
// MARK: - Finding Config Files
public static func openGenericPhpConfigFolder() {
@ -72,33 +94,37 @@ class Actions {
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder() {
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".composer/composer.json")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpConfigFolder(version: String) {
let files = [NSURL(fileURLWithPath: "\(Paths.etcPath)/php/\(version)/php.ini")]
NSWorkspace.shared.activateFileViewerSelecting(files as [URL])
}
public static func openGlobalComposerFolder() {
let file = URL(string: "file://~/.composer/composer.json".replacingTildeWithHomeDirectory)!
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openValetConfigFolder() {
let file = URL(string: "file://~/.config/valet".replacingTildeWithHomeDirectory)!
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/valet")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
public static func openPhpMonitorConfigFile() {
let file = URL(string: "file://~/.config/phpmon".replacingTildeWithHomeDirectory)!
let file = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/phpmon")
NSWorkspace.shared.activateFileViewerSelecting([file] as [URL])
}
// MARK: - Other Actions
public static func createTempPhpInfoFile() async -> URL {
try! FileSystem.writeAtomicallyToFile("/tmp/phpmon_phpinfo.php", content: "<?php phpinfo();")
public static func createTempPhpInfoFile() -> URL {
// Write a file called `phpmon_phpinfo.php` to /tmp
try! "<?php phpinfo();".write(toFile: "/tmp/phpmon_phpinfo.php", atomically: true, encoding: .utf8)
// Tell php-cgi to run the PHP and output as an .html file
await Shell.quiet("\(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")!
}
@ -117,10 +143,12 @@ class Actions {
If this does not solve the issue, the user may need to install additional
extensions and/or run `composer global update`.
*/
public static func fixMyValet() async {
await InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias)
await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated)
await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated)
await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated)
public static func fixMyValet(completed: @escaping () -> Void) {
InternalSwitcher().performSwitch(to: PhpEnv.brewPhpVersion, completion: {
brew("services restart dnsmasq", sudo: true)
brew("services restart php", sudo: true)
brew("services restart nginx", sudo: true)
completed()
})
}
}

View File

@ -7,9 +7,16 @@
import Cocoa
public class RealCommand: CommandProtocol {
public class Command {
public func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
/**
Immediately executes a command.
- Parameter path: The path of the command or program to invoke.
- Parameter arguments: A list of arguments that are passed on.
- Parameter trimNewlines: Removes empty new line output.
*/
public static func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String {
let task = Process()
task.launchPath = path
task.arguments = arguments

View File

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

View File

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

View File

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

View File

@ -17,15 +17,11 @@ public class Paths {
internal var baseDir: Paths.HomebrewDir
private var userName: String! = nil
private var userName: String
init() {
baseDir = App.architecture != "x86_64" ? .opt : .usr
}
public func loadUser() async {
let output = await Shell.pipe("id -un").out
userName = String(output.split(separator: "\n")[0])
userName = String(Shell.pipe("id -un").split(separator: "\n")[0])
}
public func detectBinaryPaths() {
@ -62,16 +58,7 @@ public class Paths {
}
public static var homePath: String {
if FileSystem is RealFileSystem {
return NSHomeDirectory()
}
if FileSystem is TestableFileSystem {
let fs = FileSystem as! TestableFileSystem
return fs.homeDirectory
}
fatalError("A valid FileSystem must be allowed to return the home path")
return NSHomeDirectory()
}
public static var cellarPath: String {
@ -95,9 +82,9 @@ public class Paths {
// (PHP Monitor will not use the user's own PATH)
private func detectComposerBinary() {
if FileSystem.fileExists("/usr/local/bin/composer") {
if Filesystem.fileExists("/usr/local/bin/composer") {
Paths.composer = "/usr/local/bin/composer"
} else if FileSystem.fileExists("/opt/homebrew/bin/composer") {
} else if Filesystem.fileExists("/opt/homebrew/bin/composer") {
Paths.composer = "/opt/homebrew/bin/composer"
} else {
Paths.composer = nil

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,8 +13,8 @@ class Alert {
onWindow window: NSWindow,
messageText: String,
informativeText: String,
buttonTitle: String = "generic.ok".localized,
secondButtonTitle: String = "generic.cancel".localized,
buttonTitle: String = "OK",
secondButtonTitle: String = "Cancel",
style: NSAlert.Style = .warning,
onFirstButtonPressed: @escaping (() -> Void)
) {

View File

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

View File

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

View File

@ -1,28 +0,0 @@
//
// System.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 01/11/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/**
Run a simple blocking Shell command on the user's own system.
Avoid using this method in favor of the fakeable Shell class unless needed for express system operations.
*/
public func system(_ command: String) -> String {
let task = Process()
task.launchPath = "/bin/sh"
task.arguments = ["-c", command]
let pipe = Pipe()
task.standardOutput = pipe
task.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output: String = NSString(data: data, encoding: String.Encoding.utf8.rawValue)! as String
return output
}

View File

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

View File

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

View File

@ -8,7 +8,7 @@
import Foundation
final class HomebrewService: Sendable, Decodable {
struct HomebrewService: Decodable, Equatable {
let name: String
let service_name: String
let running: Bool
@ -19,32 +19,10 @@ final class HomebrewService: Sendable, Decodable {
let log_path: String?
let error_log_path: String?
init(
name: String,
service_name: String,
running: Bool,
loaded: Bool,
pid: Int? = nil,
user: String? = nil,
status: String? = nil,
log_path: String? = nil,
error_log_path: String? = nil
) {
self.name = name
self.service_name = service_name
self.running = running
self.loaded = loaded
self.pid = pid
self.user = user
self.status = status
self.log_path = log_path
self.error_log_path = error_log_path
}
/**
Dummy data for preview purposes.
*/
public static func dummy(named service: String, enabled: Bool) -> HomebrewService {
public static func dummy(named service: String, enabled: Bool) -> Self {
return HomebrewService(
name: service,
service_name: service,

View File

@ -14,17 +14,15 @@ class PhpEnv {
init() {
self.currentInstall = ActivePhpInstallation()
}
func determinePhpAlias() async {
let brewPhpAlias = await Shell.pipe("\(Paths.brew) info php --json").out
let brewPhpAlias = Shell.pipe("\(Paths.brew) info php --json")
self.homebrewPackage = try! JSONDecoder().decode(
[HomebrewPackage].self,
from: brewPhpAlias.data(using: .utf8)!
).first!
Log.info("[BREW] 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
@ -45,7 +43,7 @@ class PhpEnv {
var cachedPhpInstallations: [String: PhpInstallation] = [:]
/** Information about the currently linked PHP installation. */
var currentInstall: ActivePhpInstallation!
var currentInstall: ActivePhpInstallation
/**
The version that the `php` formula via Brew is aliased to on the current system.
@ -56,9 +54,7 @@ class PhpEnv {
As such, we take that information from Homebrew.
*/
static var brewPhpAlias: String {
if Homebrew.fake { return "8.2" }
static var brewPhpVersion: String {
return Self.shared.homebrewPackage.version
}
@ -80,20 +76,17 @@ class PhpEnv {
return InternalSwitcher()
}
public static func detectPhpVersions() async {
_ = await Self.shared.detectPhpVersions()
public static func detectPhpVersions() {
_ = Self.shared.detectPhpVersions()
}
/**
Detects which versions of PHP are installed.
*/
public func detectPhpVersions() async -> [String] {
let files = await Shell.pipe("ls \(Paths.optPath) | grep php@").out
public func detectPhpVersions() -> [String] {
let files = Shell.pipe("ls \(Paths.optPath) | grep php@")
var versionsOnly = await extractPhpVersions(
from: files.components(separatedBy: "\n"),
supported: Constants.ValetSupportedPhpVersionMatrix[Valet.shared.version.major] ?? []
)
var versionsOnly = extractPhpVersions(from: files.components(separatedBy: "\n"))
// Make sure the aliased version is detected
// The user may have `php` installed, but not e.g. `php@8.0`
@ -101,7 +94,7 @@ class PhpEnv {
let phpAlias = homebrewPackage.version
// Avoid inserting a duplicate
if !versionsOnly.contains(phpAlias) && FileSystem.fileExists("\(Paths.optPath)/php/bin/php") {
if !versionsOnly.contains(phpAlias) && Filesystem.fileExists("\(Paths.optPath)/php/bin/php") {
versionsOnly.append(phpAlias)
}
@ -129,11 +122,17 @@ class PhpEnv {
*/
public func extractPhpVersions(
from versions: [String],
supported: [String],
checkBinaries: Bool = true,
generateHelpers: Bool = true
) async -> [String] {
) -> [String] {
var output: [String] = []
var supported = Constants.SupportedPhpVersions
if !Valet.enabled(feature: .supportForPhp56) {
supported.removeAll { $0 == "5.6" }
}
versions.filter { (version) -> Bool in
// Omit everything that doesn't start with php@
// (e.g. something-php@8.0 won't be detected)
@ -144,21 +143,19 @@ class PhpEnv {
// is supported and where the binary exists (avoids broken installs)
if !output.contains(version)
&& supported.contains(version)
&& (checkBinaries ? FileSystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
&& (checkBinaries ? Filesystem.fileExists("\(Paths.optPath)/php@\(version)/bin/php") : true) {
output.append(version)
}
}
if generateHelpers {
for item in output {
await PhpHelper.generate(for: item)
}
output.forEach { PhpHelper.generate(for: $0) }
}
return output
}
public func validVersions(for constraint: String) -> [VersionNumber] {
public func validVersions(for constraint: String) -> [PhpVersionNumber] {
constraint.split(separator: "|").flatMap {
return PhpVersionNumberCollection
.make(from: self.availablePhpVersions)
@ -172,9 +169,6 @@ class PhpEnv {
public func validate(_ version: String) -> Bool {
if self.currentInstall.version.short == version {
Log.info("Switching to version \(version) seems to have succeeded. Validation passed.")
Log.info("Keeping track that this is the new version!")
Stats.persistCurrentGlobalPhpVersion(version: version)
return true
}

View File

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

View File

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

View File

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

View File

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

View File

@ -35,7 +35,7 @@ class PhpConfigurationFile: CreatedFromFile {
let path = filePath.replacingOccurrences(of: "~", with: Paths.homePath)
do {
let fileContents = try FileSystem.getStringFromFile(path)
let fileContents = try String(contentsOfFile: path)
return Self.init(path: path, contents: fileContents)
} catch {
Log.warn("Could not read the PHP configuration file at: `\(filePath)`")

View File

@ -75,23 +75,16 @@ class PhpExtension {
This simply toggles the extension in the .ini file.
You may need to restart the other services in order for this change to apply.
*/
func toggle() async {
func toggle() {
let newLine = enabled
// DISABLED: Commented out line
? "; \(line)"
// ENABLED: Line where the comment delimiter (;) is removed
: line.replacingOccurrences(of: "; ", with: "")
await sed(file: file, original: line, replacement: newLine)
sed(file: file, original: line, replacement: newLine)
enabled.toggle()
if !isRunningTests {
Task { @MainActor in
MainMenu.shared.rebuild()
}
}
}
// MARK: - Static Methods

View File

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

View File

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

View File

@ -18,6 +18,6 @@ protocol PhpSwitcherDelegate: AnyObject {
protocol PhpSwitcher {
func performSwitch(to version: String) async
func performSwitch(to version: String, completion: @escaping () -> Void)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,8 @@ extension AppDelegate {
Please note that PHP Monitor needs to be running in the background for this to work.
*/
@MainActor func application(_ application: NSApplication, open urls: [URL]) {
func application(_ application: NSApplication, open urls: [URL]) {
if !Preferences.isEnabled(.allowProtocolForIntegrations) {
Log.info("Acting on commands via phpmon:// has been disabled.")
return

View File

@ -33,17 +33,15 @@ extension AppDelegate {
}
@IBAction func reloadDomainListPressed(_ sender: Any) {
Task { // Reload domains
let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
let vc = App.shared.domainListWindowController?
.window?.contentViewController as? DomainListVC
if vc != nil {
// If the view exists, directly reload the list of sites.
await vc!.reloadDomains()
} else {
// If the view does not exist, reload the cached data that was populated when the app launched.
await Valet.shared.reloadSites()
}
if vc != nil {
// If the view exists, directly reload the list of sites
vc!.reloadDomains()
} else {
// If the view does not exist, reload the cached data that was populated when the app initially launched.
Valet.shared.reloadSites()
}
}

View File

@ -13,6 +13,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
// MARK: - Variables
/**
The Shell singleton that keeps track of the history of all
(invoked by PHP Monitor) shell commands. It is used to
invoke all commands in this application.
*/
let sharedShell: Shell
/**
The App singleton contains information about the state of
the application and global variables.
@ -57,24 +64,18 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
*/
override init() {
logger.verbosity = .info
#if DEBUG
logger.verbosity = .performance
if let profile = CommandLine.arguments.first(where: { $0.matches(pattern: "--configuration:*") }) {
Self.initializeTestingProfile(profile.replacingOccurrences(of: "--configuration:", with: ""))
}
logger.verbosity = .performance
#endif
if CommandLine.arguments.contains("--v") {
logger.verbosity = .performance
Log.info("Extra verbose mode has been activated.")
}
Log.separator(as: .info)
Log.info("PHP MONITOR by Nico Verbruggen")
Log.info("Version \(App.version)")
Log.separator(as: .info)
self.sharedShell = Shell.user
self.state = App.shared
self.menu = MainMenu.shared
self.paths = Paths.shared
@ -86,11 +87,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
self.phpEnvironment = PhpEnv.shared
}
static func initializeTestingProfile(_ path: String) {
Log.info("The configuration with path `\(path)` is being requested...")
TestableConfiguration.loadFrom(path: path).apply()
}
// MARK: - Lifecycle
/**
@ -101,10 +97,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Make sure notifications will work
setupNotifications()
Task { // Make sure the menu performs its initial checks
await paths.loadUser()
await menu.startup()
}
// Make sure the menu performs its initial checks
Task { await menu.startup() }
}
}

View File

@ -21,7 +21,7 @@ class AppUpdateChecker {
public static func retrieveVersionFromCask(
_ initiatedFromBackground: Bool = true
) async -> String {
) -> String {
let caskFile = App.version.contains("-dev")
? Constants.Urls.DevBuildCaskFile.absoluteString
: Constants.Urls.StableBuildCaskFile.absoluteString
@ -32,14 +32,14 @@ class AppUpdateChecker {
command = "curl -s --max-time 5"
}
return await Shell.pipe(
return Shell.pipe(
"\(command) '\(caskFile)' | grep version"
).out
)
}
public static func checkIfNewerVersionIsAvailable(
initiatedFromBackground: Bool = true
) async {
) {
if initiatedFromBackground {
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Automatic updates are disabled. No check will be performed.")
@ -49,7 +49,7 @@ class AppUpdateChecker {
Log.info("Automatic updates are enabled, a check will be performed.")
}
let versionString = await retrieveVersionFromCask(initiatedFromBackground)
let versionString = retrieveVersionFromCask(initiatedFromBackground)
guard let onlineVersion = AppVersion.from(versionString) else {
Log.err("We couldn't check for updates!")
@ -119,13 +119,13 @@ class AppUpdateChecker {
}
private static func notifyVersionDoesNotNeedUpgrade() {
Task { @MainActor in
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.is_latest_version.title".localized,
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
description: ""
)
.withPrimary(text: "generic.ok".localized)
.withPrimary(text: "OK")
.show()
}
}
@ -134,7 +134,7 @@ class AppUpdateChecker {
let devSuffix = isDev ? "-dev" : ""
let command = isDev ? "brew upgrade phpmon-dev" : "brew upgrade phpmon"
Task { @MainActor in
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.newer_version_available.title".localized(version.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle".localized,
@ -160,7 +160,7 @@ class AppUpdateChecker {
}
private static func notifyAboutConnectionIssue() {
Task { @MainActor in
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "updater.alerts.cannot_check_for_update.title".localized,
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
@ -174,7 +174,7 @@ class AppUpdateChecker {
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}
)
.withPrimary(text: "generic.ok".localized)
.withPrimary(text: "OK")
.show()
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="20037"/>
<capability name="Image references" minToolsVersion="12.0"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Search Toolbar Item" minToolsVersion="12.0" minSystemVersion="11.0"/>
@ -1112,17 +1112,17 @@ Gw
<objects>
<viewController storyboardIdentifier="newProxyLink" id="dwh-CF-6iv" customClass="AddProxyVC" customModule="PHP_Monitor" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="U5U-QR-YXS">
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<box boxType="custom" borderWidth="0.0" title="Box" translatesAutoresizingMaskIntoConstraints="NO" id="kkd-UV-SnA">
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<view key="contentView" id="IXW-35-8NJ">
<rect key="frame" x="0.0" y="0.0" width="540" height="286"/>
<rect key="frame" x="0.0" y="0.0" width="480" height="286"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="QCK-Z9-w7g">
<rect key="frame" x="20" y="196" width="500" height="21"/>
<rect key="frame" x="20" y="196" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" title="http://127.0.0.1:80" placeholderString="http://127.0.0.1:80" drawsBackground="YES" id="muS-8M-KSy">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -1149,7 +1149,7 @@ Gw
</textFieldCell>
</textField>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SNw-oQ-bnb">
<rect key="frame" x="20" y="147" width="500" height="21"/>
<rect key="frame" x="20" y="147" width="440" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Enter a domain name here." drawsBackground="YES" id="gTQ-Y2-Y9w">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
@ -1176,7 +1176,7 @@ Gw
<color key="fillColor" name="windowBackgroundColor" catalog="System" colorSpace="catalog"/>
</box>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4Vi-cN-ude">
<rect key="frame" x="377" y="13" width="150" height="32"/>
<rect key="frame" x="317" y="13" width="150" height="32"/>
<buttonCell key="cell" type="push" title="[i18n] Create Proxy" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="H2Z-c5-5Vk">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
@ -1205,10 +1205,7 @@ Gw
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSZ-x8-Pqi">
<rect key="frame" x="18" y="128" width="504" height="14"/>
<constraints>
<constraint firstAttribute="width" relation="lessThanOrEqual" constant="500" id="sF1-RG-URI"/>
</constraints>
<rect key="frame" x="18" y="128" width="444" height="14"/>
<textFieldCell key="cell" title="[i18n] Preview text here" id="ISE-9R-ncQ">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@ -1226,7 +1223,7 @@ Gw
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="5x7-ll-2f7">
<rect key="frame" x="18" y="60" width="504" height="28"/>
<rect key="frame" x="18" y="60" width="444" height="28"/>
<textFieldCell key="cell" title="[i18n] Securing a domain requires administrative privileges. You may be prompted for your password or Touch ID." id="IMB-O5-ZOy">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
@ -1242,7 +1239,7 @@ Gw
</textFieldCell>
</textField>
<textField hidden="YES" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="w0k-CK-0u4">
<rect key="frame" x="191" y="23" width="180" height="14"/>
<rect key="frame" x="131" y="23" width="180" height="14"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="That domain name already exists." id="4sH-94-UJl">
<font key="font" metaFont="smallSystem"/>
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
@ -1296,7 +1293,7 @@ Gw
</viewController>
<customObject id="VaP-ZM-OcY" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="220" y="1522"/>
<point key="canvasLocation" x="210" y="1524"/>
</scene>
<!--Window Controller-->
<scene sceneID="5Gf-7O-tdA">

View File

@ -1,36 +0,0 @@
//
// EnvironmentManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 14/09/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
public class EnvironmentManager {
var values: [EnvironmentProperty: Bool] = [:]
public func process() async {
self.values[.hasValetInstalled] = await !{
let output = await Shell.pipe("valet --version").out
// Failure condition #1: does not contain Laravel Valet
if !output.contains("Laravel Valet") {
return true
}
// Extract the version number
Valet.shared.version = try! VersionNumber.parse(VersionExtractor.from(output)!)
// Get the actual version
return Valet.shared.version == nil
}() // returns true if none of the failure conditions are met
}
}
public enum EnvironmentProperty {
case hasHomebrewInstalled
case hasValetInstalled
}

View File

@ -21,39 +21,51 @@ class InterApp {
let action: (String) -> Void
}
@MainActor static func getCommands() -> [InterApp.Action] { return [
static func getCommands() -> [InterApp.Action] { return [
InterApp.Action(command: "list", action: { _ in
DomainListVC.show()
}),
InterApp.Action(command: "services/stop", action: { _ in
Task { MainMenu.shared.stopValetServices() }
MainMenu.shared.stopValetServices()
}),
InterApp.Action(command: "services/restart/all", action: { _ in
Task { MainMenu.shared.restartValetServices() }
MainMenu.shared.restartValetServices()
}),
InterApp.Action(command: "services/restart/nginx", action: { _ in
Task { MainMenu.shared.restartNginx() }
MainMenu.shared.restartNginx()
}),
InterApp.Action(command: "services/restart/php", action: { _ in
Task { MainMenu.shared.restartPhpFpm() }
MainMenu.shared.restartPhpFpm()
}),
InterApp.Action(command: "services/restart/dnsmasq", action: { _ in
Task { MainMenu.shared.restartDnsMasq() }
MainMenu.shared.restartDnsMasq()
}),
InterApp.Action(command: "locate/config", action: { _ in
Task { MainMenu.shared.openActiveConfigFolder() }
MainMenu.shared.openActiveConfigFolder()
}),
InterApp.Action(command: "locate/composer", action: { _ in
Task { MainMenu.shared.openGlobalComposerFolder() }
MainMenu.shared.openGlobalComposerFolder()
}),
InterApp.Action(command: "locate/valet", action: { _ in
Task { MainMenu.shared.openValetConfigFolder() }
MainMenu.shared.openValetConfigFolder()
}),
InterApp.Action(command: "phpinfo", action: { _ in
Task { MainMenu.shared.openPhpInfo() }
MainMenu.shared.openPhpInfo()
}),
InterApp.Action(command: "switch/php/", action: { version in
Task { MainMenu.shared.switchToAnyPhpVersion(version) }
if PhpEnv.shared.availablePhpVersions.contains(version) {
MainMenu.shared.switchToPhpVersion(version)
} else {
DispatchQueue.main.async {
BetterAlert().withInformation(
title: "alert.php_switch_unavailable.title".localized,
subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
).withPrimary(
text: "alert.php_switch_unavailable.ok".localized
).show()
}
}
})
]}
}

View File

@ -1,89 +0,0 @@
//
// FakeServicesManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/12/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
class FakeServicesManager: ServicesManager {
var fixedFormulae: [String] = []
var fixedStatus: Service.Status = .active
override init() {}
init(
formulae: [String] = ["php", "nginx", "dnsmasq"],
status: Service.Status = .active,
loading: Bool = false
) {
super.init()
Log.warn("A fake services manager is being used, so Homebrew formula resolver is set to act in fake mode.")
Log.warn("If you do not want this behaviour, do not make use of a `FakeServicesManager`!")
self.fixedFormulae = formulae
self.fixedStatus = status
self.services = []
self.reapplyServices()
if loading {
return
}
Task { @MainActor in
self.firstRunComplete = true
}
}
private func reapplyServices() {
let services = self.formulae.map {
let wrapper = Service(
formula: $0,
service: HomebrewService.dummy(named: $0.name, enabled: self.fixedStatus == .active)
)
return wrapper
}
Task { @MainActor in
self.services = services
}
}
override var formulae: [HomebrewFormula] {
return fixedFormulae.map { formula in
return HomebrewFormula.init(formula, elevated: false)
}
}
override func reloadServicesStatus() async {
await delay(seconds: 0.3)
self.reapplyServices()
}
override func toggleService(named: String) async {
await delay(seconds: 0.3)
let services = services.map({ service in
let newServiceEnabled = service.name == named
? service.status != .active // inverse (i.e. if active -> becomes inactive)
: service.status == .active // service remains unmodified if it's not the named one we change
return Service(
formula: service.formula,
service: HomebrewService.dummy(
named: service.name,
enabled: newServiceEnabled
)
)
})
Task { @MainActor in
self.services = services
}
}
}

View File

@ -1,55 +0,0 @@
//
// ServiceWrapper.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/12/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
/** Service linked to a Homebrew formula and whether it is currently (in)active or missing. */
public struct Service: Hashable {
var formula: HomebrewFormula
var status: Status = .missing
public var name: String {
return formula.name
}
init(formula: HomebrewFormula, service: HomebrewService? = nil) {
self.formula = formula
guard let service else { return }
self.status = service.running ? .active : .inactive
if service.status == "error" {
self.status = .error
}
}
// MARK: - Protocols
public static func == (lhs: Service, rhs: Service) -> Bool {
return lhs.hashValue == rhs.hashValue
}
public func hash(into hasher: inout Hasher) {
hasher.combine(formula)
hasher.combine(status)
}
// MARK: - Status
public enum Status: String {
case active
case inactive
case error
case missing
var asBool: Bool {
return self == .active
}
}
}

View File

@ -1,131 +0,0 @@
//
// ServicesManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
class ServicesManager: ObservableObject {
@ObservedObject static var shared: ServicesManager = ValetServicesManager()
@Published var services = [Service]()
@Published var firstRunComplete: Bool = false
public static func useFake() {
ServicesManager.shared = FakeServicesManager.init(
formulae: ["php", "nginx", "dnsmasq", "mysql"],
status: .active
)
}
/**
The order of services is important, so easy access is accomplished
without much fanfare through subscripting.
*/
subscript(name: String) -> Service? {
return self.services.first { wrapper in
wrapper.name == name
}
}
public var hasError: Bool {
if self.services.isEmpty || !self.firstRunComplete {
return false
}
return self.services[0...2]
.map { $0.status }
.contains(.error)
}
public var statusMessage: String {
if self.services.isEmpty || !self.firstRunComplete {
return "Loading..."
}
let statuses = self.services[0...2].map { $0.status }
if statuses.contains(.missing) {
return "A key service is not installed."
}
if statuses.contains(.error) {
return "A key service is reporting an error state."
}
if statuses.contains(.inactive) {
return "A key service is not running."
}
return "All Valet services are OK."
}
public var statusColor: Color {
if self.services.isEmpty || !self.firstRunComplete {
return .yellow
}
let statuses = self.services[0...2].map { $0.status }
if statuses.contains(.missing)
|| statuses.contains(.inactive)
|| statuses.contains(.error) {
return .red
}
return .green
}
/**
This method is called when the system configuration has changed
and all the status of one or more services may need to be determined.
*/
public func reloadServicesStatus() async {
fatalError("This method `\(#function)` has not been implemented")
}
/**
This method is called when a service needs to be toggled (on/off).
*/
public func toggleService(named: String) async {
fatalError("This method `\(#function)` has not been implemented")
}
/**
This method will notify all publishers that subscribe to notifiable objects.
The notified objects include this very ServicesManager as well as any individual service instances.
*/
public func broadcastServicesUpdated() {
Task { @MainActor in
self.objectWillChange.send()
}
}
var formulae: [HomebrewFormula] {
var formulae = [
Homebrew.Formulae.php,
Homebrew.Formulae.nginx,
Homebrew.Formulae.dnsmasq
]
let additionalFormulae = (Preferences.custom.services ?? []).map({ item in
return HomebrewFormula(item, elevated: false)
})
formulae.append(contentsOf: additionalFormulae)
return formulae
}
init() {
Log.info("The services manager will determine which Valet services exist on this system.")
services = formulae.map {
Service(formula: $0)
}
}
}

View File

@ -1,157 +0,0 @@
//
// ValetServicesManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 23/12/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
class ValetServicesManager: ServicesManager {
override init() {
super.init()
// Load the initial services state
Task {
await self.reloadServicesStatus()
Task { @MainActor in
firstRunComplete = true
}
}
}
/**
The last known state of all Homebrew services.
*/
var homebrewServices: [HomebrewService] = []
/**
This method allows us to reload the Homebrew services, but we run this command
twice (once for user services, and once for root services). Please note that
these two commands are executed concurrently.
*/
override func reloadServicesStatus() async {
await withTaskGroup(of: [HomebrewService].self, body: { group in
// First, retrieve the status of the formulae that run as root
group.addTask {
let rootServiceNames = self.formulae
.filter { $0.elevated }
.map { $0.name }
let rootJson = await Shell
.pipe("sudo \(Paths.brew) services info --all --json")
.out.data(using: .utf8)!
return try! JSONDecoder()
.decode([HomebrewService].self, from: rootJson)
.filter({ return rootServiceNames.contains($0.name) })
}
// At the same time, retrieve the status of the formulae that run as user
group.addTask {
let userServiceNames = self.formulae
.filter { !$0.elevated }
.map { $0.name }
let normalJson = await Shell
.pipe("\(Paths.brew) services info --all --json")
.out.data(using: .utf8)!
return try! JSONDecoder()
.decode([HomebrewService].self, from: normalJson)
.filter({ return userServiceNames.contains($0.name) })
}
// Ensure that Homebrew services' output is stored
self.homebrewServices = []
for await services in group {
homebrewServices.append(contentsOf: services)
}
// Dispatch the update of the new service wrappers
Task { @MainActor in
// Ensure both commands complete (but run concurrently)
services = formulae.map { formula in
Service(
formula: formula,
service: homebrewServices.first(where: { service in
service.name == formula.name
})
)
}
// Broadcast that all services have been updated
self.broadcastServicesUpdated()
}
})
}
override func toggleService(named: String) async {
guard let wrapper = self[named] else {
return Log.err("The wrapper for '\(named)' is missing.")
}
// Normally, we allow starting and stopping
var action = wrapper.status == .active ? "stop" : "start"
// However, if we've encountered an error, attempt to restart
if wrapper.status == .error {
action = "restart"
}
// Run the command
await brew(
"services \(action) \(wrapper.formula.name)",
sudo: wrapper.formula.elevated
)
// Reload the services status to confirm this worked
await ServicesManager.shared.reloadServicesStatus()
Task {
await presentTroubleshootingForService(named: named)
}
}
@MainActor func presentTroubleshootingForService(named: String) {
let after = self.homebrewServices.first { service in
return service.name == named
}
guard let after else { return }
if after.status == "error" {
Log.err("The service '\(named)' is now reporting an error.")
guard let errorLogPath = after.error_log_path else {
return BetterAlert().withInformation(
title: "alert.service_error.title".localized(named),
subtitle: "alert.service_error.subtitle.no_error_log".localized(named),
description: "alert.service_error.extra".localized
)
.withPrimary(text: "alert.service_error.button.close".localized)
.show()
}
BetterAlert().withInformation(
title: "alert.service_error.title".localized(named),
subtitle: "alert.service_error.subtitle.error_log".localized(named),
description: "alert.service_error.extra".localized
)
.withPrimary(text: "alert.service_error.button.close".localized)
.withTertiary(text: "alert.service_error.button.show_log".localized, action: { alert in
let url = URL(fileURLWithPath: errorLogPath)
if errorLogPath.hasSuffix(".log") {
NSWorkspace.shared.open(url)
} else {
NSWorkspace.shared.activateFileViewerSelecting([url])
}
alert.close(with: .OK)
})
.show()
}
}
}

View File

@ -0,0 +1,80 @@
//
// ServicesManager.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 11/06/2022.
// Copyright © 2022 Nico Verbruggen. All rights reserved.
//
import Foundation
import SwiftUI
class ServicesManager: ObservableObject {
static var shared = ServicesManager()
@Published var rootServices: [String: HomebrewService] = [:]
@Published var userServices: [String: HomebrewService] = [:]
public static func loadHomebrewServices(completed: (() -> Void)? = nil) {
let rootServiceNames = [
PhpEnv.phpInstall.formula,
"nginx",
"dnsmasq"
]
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 rootServiceNames.contains($0.name) })
DispatchQueue.main.async {
ServicesManager.shared.rootServices = Dictionary(
uniqueKeysWithValues: services.map { ($0.name, $0) }
)
}
}
guard let userServiceNames = Preferences.custom.services else {
return
}
DispatchQueue.global(qos: .background).async {
let data = Shell
.pipe("\(Paths.brew) services info --all --json", requiresPath: true)
.data(using: .utf8)!
let services = try! JSONDecoder()
.decode([HomebrewService].self, from: data)
.filter({ return userServiceNames.contains($0.name) })
DispatchQueue.main.async {
ServicesManager.shared.userServices = Dictionary(
uniqueKeysWithValues: services.map { ($0.name, $0) }
)
completed?()
}
}
}
func loadData() {
Self.loadHomebrewServices()
}
/**
Dummy data for preview purposes.
*/
func withDummyServices(_ services: [String: Bool]) -> Self {
for (service, enabled) in services {
let item = HomebrewService.dummy(named: service, enabled: enabled)
self.rootServices[service] = item
}
return self
}
}

View File

@ -29,7 +29,7 @@ class Startup {
// If we get here, something's gone wrong and the check has failed...
Log.info("[FAIL] \(check.name)")
await showAlert(for: check)
showAlert(for: check)
return false
}
@ -45,27 +45,29 @@ class Startup {
- ones that require an app restart, which prompt the user to exit the app
- ones that allow the app to continue, which allow the user to retry
*/
@MainActor private func showAlert(for check: EnvironmentCheck) {
if check.requiresAppRestart {
private func showAlert(for check: EnvironmentCheck) {
DispatchQueue.main.async {
if check.requiresAppRestart {
BetterAlert()
.withInformation(
title: check.titleText,
subtitle: check.subtitleText,
description: check.descriptionText
)
.withPrimary(text: check.buttonText, action: { _ in
exit(1)
}).show()
}
BetterAlert()
.withInformation(
title: check.titleText,
subtitle: check.subtitleText,
description: check.descriptionText
)
.withPrimary(text: check.buttonText, action: { _ in
exit(1)
}).show()
.withPrimary(text: "OK")
.show()
}
BetterAlert()
.withInformation(
title: check.titleText,
subtitle: check.subtitleText,
description: check.descriptionText
)
.withPrimary(text: "generic.ok".localized)
.show()
}
/**
@ -73,7 +75,7 @@ class Startup {
initialized when it is done working. The switcher must be initialized on the main thread.
*/
private func initializeSwitcher() {
Task { @MainActor in
DispatchQueue.main.async {
let appDelegate = NSApplication.shared.delegate as! AppDelegate
appDelegate.initializeSwitcher()
}
@ -86,7 +88,7 @@ class Startup {
// The Homebrew binary must exist.
// =================================================================================
EnvironmentCheck(
command: { return !FileSystem.fileExists(Paths.brew) },
command: { return !FileManager.default.fileExists(atPath: Paths.brew) },
name: "`\(Paths.brew)` exists",
titleText: "alert.homebrew_missing.title".localized,
subtitleText: "alert.homebrew_missing.subtitle".localized,
@ -103,7 +105,7 @@ class Startup {
// The PHP binary must exist.
// =================================================================================
EnvironmentCheck(
command: { return !FileSystem.fileExists(Paths.php) },
command: { return !Filesystem.fileExists(Paths.php) },
name: "`\(Paths.php)` exists",
titleText: "startup.errors.php_binary.title".localized,
subtitleText: "startup.errors.php_binary.subtitle".localized,
@ -113,9 +115,7 @@ class Startup {
// Make sure we can detect one or more PHP installations.
// =================================================================================
EnvironmentCheck(
command: {
return await !Shell.pipe("ls \(Paths.optPath) | grep php").out.contains("php")
},
command: { return !Shell.pipe("ls \(Paths.optPath) | grep php").contains("php") },
name: "`ls \(Paths.optPath) | grep php` returned php result",
titleText: "startup.errors.php_opt.title".localized,
subtitleText: "startup.errors.php_opt.subtitle".localized(
@ -128,7 +128,7 @@ class Startup {
// =================================================================================
EnvironmentCheck(
command: {
return !(FileSystem.fileExists(Paths.valet) || FileSystem.fileExists("~/.composer/vendor/bin/valet"))
return !(Filesystem.fileExists(Paths.valet) || Filesystem.fileExists("~/.composer/vendor/bin/valet"))
},
name: "`valet` binary exists",
titleText: "startup.errors.valet_executable.title".localized,
@ -143,14 +143,14 @@ class Startup {
// functioning correctly. Let the user know that they need to run `valet trust`.
// =================================================================================
EnvironmentCheck(
command: { return await !Shell.pipe("cat /private/etc/sudoers.d/brew").out.contains(Paths.brew) },
command: { return !Shell.pipe("cat /private/etc/sudoers.d/brew").contains(Paths.brew) },
name: "`/private/etc/sudoers.d/brew` contains brew",
titleText: "startup.errors.sudoers_brew.title".localized,
subtitleText: "startup.errors.sudoers_brew.subtitle".localized,
descriptionText: "startup.errors.sudoers_brew.desc".localized
),
EnvironmentCheck(
command: { return await !Shell.pipe("cat /private/etc/sudoers.d/valet").out.contains(Paths.valet) },
command: { return !Shell.pipe("cat /private/etc/sudoers.d/valet").contains(Paths.valet) },
name: "`/private/etc/sudoers.d/valet` contains valet",
titleText: "startup.errors.sudoers_valet.title".localized,
subtitleText: "startup.errors.sudoers_valet.subtitle".localized,
@ -160,10 +160,7 @@ class Startup {
// Verify if the Homebrew services are running (as root).
// =================================================================================
EnvironmentCheck(
command: {
await HomebrewDiagnostics.loadInstalledTaps()
return await HomebrewDiagnostics.cannotLoadService("dnsmasq")
},
command: { return HomebrewDiagnostics.cannotLoadService() },
name: "`sudo \(Paths.brew) services info` JSON loaded",
titleText: "startup.errors.services_json_error.title".localized,
subtitleText: "startup.errors.services_json_error.subtitle".localized,
@ -174,7 +171,7 @@ class Startup {
// =================================================================================
EnvironmentCheck(
command: {
return !FileSystem.directoryExists("~/.config/valet")
return !Filesystem.directoryExists("~/.config/valet")
},
name: "`.config/valet` not empty (Valet installed)",
titleText: "startup.errors.valet_not_installed.title".localized,
@ -203,10 +200,10 @@ class Startup {
// =================================================================================
EnvironmentCheck(
command: {
let nodePath = await Shell.pipe("which node").out
return App.architecture == "x86_64"
&& FileSystem.fileExists("/usr/local/bin/which")
&& nodePath.contains("env: node: No such file or directory")
&& FileManager.default.fileExists(atPath: "/usr/local/bin/which")
&& Shell.pipe("which node", requiresPath: false)
.contains("env: node: No such file or directory")
},
name: "`env: node` issue does not apply",
titleText: "startup.errors.which_alias_issue.title".localized,
@ -218,7 +215,7 @@ class Startup {
// =================================================================================
EnvironmentCheck(
command: {
return await Shell.pipe("valet --version").out
return valet("--version", sudo: false)
.contains("Composer detected issues in your platform")
},
name: "`no global composer issues",
@ -231,7 +228,7 @@ class Startup {
// =================================================================================
EnvironmentCheck(
command: {
let output = await Shell.pipe("valet --version").out
let output = valet("--version", sudo: false)
// Failure condition #1: does not contain Laravel Valet
if !output.contains("Laravel Valet") {
return true
@ -242,7 +239,7 @@ class Startup {
.components(separatedBy: "Laravel Valet")[1]
.trimmingCharacters(in: .whitespaces)
// Extract the version number
Valet.shared.version = try! VersionNumber.parse(VersionExtractor.from(output)!)
Valet.shared.version = VersionExtractor.from(output)
// Get the actual version
return Valet.shared.version == nil
},
@ -250,19 +247,6 @@ class Startup {
titleText: "startup.errors.valet_version_unknown.title".localized,
subtitleText: "startup.errors.valet_version_unknown.subtitle".localized,
descriptionText: "startup.errors.valet_version_unknown.desc".localized
),
// =================================================================================
// Ensure the Valet version is supported.
// =================================================================================
EnvironmentCheck(
command: {
// We currently support Valet 2, 3 or 4. Any other version should get an alert.
return ![2, 3, 4].contains(Valet.shared.version.major)
},
name: "valet version is supported",
titleText: "startup.errors.valet_version_not_supported.title".localized,
subtitleText: "startup.errors.valet_version_not_supported.subtitle".localized,
descriptionText: "startup.errors.valet_version_not_supported.desc".localized
)
]
}

View File

@ -65,20 +65,17 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
@IBAction func pressedCreateProxy(_ sender: Any) {
let domain = self.inputDomainName.stringValue
let proxyName = self.inputProxySubject.stringValue
let secure = (self.buttonSecure.state == .on)
let secure = self.buttonSecure.state == .on ? " --secure" : ""
dismissView(outcome: .OK)
App.shared.domainListWindowController?.contentVC.setUIBusy()
Task { // Ensure we proxy the site asynchronously and reload UI on main thread again
try! await ValetInteractor.shared.proxy(
domain: domain,
proxy: proxyName,
secure: secure
)
DispatchQueue.global(qos: .userInitiated).async {
Shell.run("\(Paths.valet) proxy \(domain) \(proxyName)\(secure)", requiresPath: true)
Actions.restartNginx()
Task { @MainActor in
DispatchQueue.main.async {
App.shared.domainListWindowController?.contentVC.setUINotBusy()
App.shared.domainListWindowController?.pressedReload(nil)
}
@ -160,14 +157,8 @@ class AddProxyVC: NSViewController, NSTextFieldDelegate {
return
}
var translationKey = "domain_list.add.proxy_available"
if inputProxySubject.stringValue.starts(with: "https://") {
translationKey = "domain_list.add.proxy_https_warning"
}
previewText.stringValue =
translationKey.localized(
previewText.stringValue = "domain_list.add.proxy_available"
.localized(
inputProxySubject.stringValue,
buttonSecure.state == .on ? "https" : "http",
inputDomainName.stringValue,

View File

@ -51,11 +51,11 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
// MARK: - Outlet Interactions
func createLink() async {
@IBAction func pressedCreateLink(_ sender: Any) {
let path = pathControl.url!.path
let name = inputDomainName.stringValue
if !FileSystem.anyExists(path) {
if !Filesystem.exists(path) {
Alert.confirm(
onWindow: view.window!,
messageText: "domain_list.alert.folder_missing.title".localized,
@ -70,28 +70,23 @@ class AddSiteVC: NSViewController, NSTextFieldDelegate {
}
// Adding `valet links` is a workaround for Valet malforming the config.json file
Task {
try! await ValetInteractor.shared.link(path: path, domain: name)
// TODO: I will have to investigate and report this behaviour if possible
Shell.run("cd '\(path)' && \(Paths.valet) link '\(name)' && valet links", requiresPath: true)
dismissView(outcome: .OK)
dismissView(outcome: .OK)
// Reset search
App.shared.domainListWindowController?
.searchToolbarItem
.searchField.stringValue = ""
// Reset search
App.shared.domainListWindowController?
.searchToolbarItem
.searchField.stringValue = ""
// Add the new item and scrolls to it
await App.shared.domainListWindowController?
.contentVC
.addedNewSite(
name: name,
secureAfterLinking: buttonSecure.state == .on
)
}
}
@IBAction func pressedCreateLink(_ sender: Any) {
Task { await createLink() }
// Add the new item and scrolls to it
App.shared.domainListWindowController?
.contentVC
.addedNewSite(
name: name,
secure: buttonSecure.state == .on
)
}
@IBAction func pressedCancel(_ sender: Any) {

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