Compare commits
1 Commits
Author | SHA1 | Date | |
---|---|---|---|
a407515534 |
@ -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>
|
@ -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 = ""
|
||||
|
@ -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>
|
57
README.md
@ -79,65 +79,20 @@ 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>
|
||||
|
@ -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 you’re 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 |
|
||||
|
@ -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
@ -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>
|
@ -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")
|
||||
}
|
||||
*/
|
||||
}
|
@ -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)
|
@ -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")!
|
||||
|
@ -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
|
@ -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
|
332
phpmon-tests/Test Files/brew/brew-formula.json
Normal 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
|
||||
}
|
||||
]
|
@ -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.
|
@ -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")
|
@ -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)
|
29
phpmon-tests/Versions/PhpVersionDetectionTest.swift
Normal 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"])
|
||||
}
|
||||
}
|
@ -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"])
|
@ -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"))
|
||||
}
|
||||
|
@ -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)
|
@ -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
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 575 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 41 KiB |
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
|
33
phpmon/Common/Core/Shell+PATH.swift
Normal 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) ?? ""
|
||||
}
|
||||
}
|
171
phpmon/Common/Core/Shell.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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 ""
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
) {
|
||||
|
@ -33,46 +33,31 @@ class Application {
|
||||
Attempt to open a specific directory in the app of choice.
|
||||
(This will open the app if it isn't open yet.)
|
||||
*/
|
||||
@objc public func openDirectory(file: String) async {
|
||||
return await Shell.quiet("/usr/bin/open -a \"\(name)\" \"\(file)\"")
|
||||
@objc public func openDirectory(file: String) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
61
phpmon/Common/Helpers/Filesystem.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -30,7 +30,7 @@ class PMWindowController: NSWindowController, NSWindowDelegate {
|
||||
}
|
||||
|
||||
deinit {
|
||||
Log.perf("deinit: \(String(describing: self)).\(#function)")
|
||||
Log.perf("Window controller '\(windowName)' was deinitialized")
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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")
|
||||
}
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
208
phpmon/Common/PHP/PHP Version/PhpVersionNumber.swift
Normal 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
|
||||
}
|
||||
}
|
@ -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 []
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)`")
|
||||
|
@ -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
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
||||
|
||||
class PhpInstallation {
|
||||
|
||||
var versionNumber: VersionNumber
|
||||
var versionNumber: PhpVersionNumber
|
||||
|
||||
/**
|
||||
In order to determine details about a PHP installation, we’ll 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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).")
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,6 @@ protocol PhpSwitcherDelegate: AnyObject {
|
||||
|
||||
protocol PhpSwitcher {
|
||||
|
||||
func performSwitch(to version: String) async
|
||||
func performSwitch(to version: String, completion: @escaping () -> Void)
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -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()
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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]!
|
||||
}
|
||||
}
|
@ -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)!
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() }
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
})
|
||||
]}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
80
phpmon/Domain/App/ServicesManager.swift
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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(Valet.shared.version.text),
|
||||
descriptionText: "startup.errors.valet_version_not_supported.desc".localized
|
||||
)
|
||||
]
|
||||
}
|
||||
|
@ -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,
|
||||
|