mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-07 20:10:08 +02:00
Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e319cd50f | |||
595dc8c028 | |||
f7b1679e97 | |||
9f1761d68e | |||
871480d70c | |||
2b1c1c12f8 | |||
a22346ed35 | |||
e3fa34d4f9 | |||
3d225ea79f | |||
d2cd387c18 | |||
48bb782e33 | |||
9710ffa8da | |||
46408f5ee5 | |||
2c39f1db8b | |||
f20286cbd9 | |||
f1fe42e563 | |||
9778fd5c7b | |||
dba2ce5bf3 | |||
4644c1ada4 |
@ -51,6 +51,12 @@ Once you have downloaded this repository, open `PHP Monitor.xcodeproj`, and you
|
||||
|
||||
If you'd like to create a production build, choose "Any Mac" as the target and select Product > Archive.
|
||||
|
||||
## ✅ Testing
|
||||
|
||||
In order to properly test everything, you will want to use the _PHP Monitor DEV_ target. There are unit and UI tests both.
|
||||
|
||||
You may sporadically see failures in UI tests due to the following error: `Invalid parameter not satisfying: point.x != INFINITY && point.y != INFINITY`. This seems to be an issue with Xcode that Apple may need to resolve?
|
||||
|
||||
## 🚀 Release procedure
|
||||
|
||||
1. Merge into `main`
|
||||
|
@ -2348,7 +2348,7 @@
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1420;
|
||||
LastUpgradeCheck = 1530;
|
||||
LastUpgradeCheck = 1610;
|
||||
ORGANIZATIONNAME = "Nico Verbruggen";
|
||||
TargetAttributes = {
|
||||
C406A5EF298AD2CE00B5B85A = {
|
||||
@ -3688,7 +3688,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1516;
|
||||
CURRENT_PROJECT_VERSION = 1525;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -3719,7 +3719,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1516;
|
||||
CURRENT_PROJECT_VERSION = 1525;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG = NO;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -3960,7 +3960,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1516;
|
||||
CURRENT_PROJECT_VERSION = 1525;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG = NO;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -4077,7 +4077,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1516;
|
||||
CURRENT_PROJECT_VERSION = 1525;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -4194,7 +4194,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1516;
|
||||
CURRENT_PROJECT_VERSION = 1525;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG = YES;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
@ -4377,7 +4377,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 1516;
|
||||
CURRENT_PROJECT_VERSION = 1525;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG = NO;
|
||||
DEVELOPMENT_TEAM = 8M54J5J787;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1540"
|
||||
LastUpgradeVersion = "1610"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
@ -22,7 +22,7 @@ You can also add new domains as links, isolate sites, manage various services, a
|
||||
PHP Monitor is a universal application that runs natively on Apple Silicon **and** Intel-based Macs.
|
||||
|
||||
* Your user account can administer your computer (required for some functionality, e.g. certificate generation)
|
||||
* macOS 12.4 or later (Monterey, Ventura and Sonoma are supported)
|
||||
* macOS 12.4 or later
|
||||
* Homebrew is installed in the default location (`/usr/local/homebrew` or `/opt/homebrew`)
|
||||
* Homebrew `php` formula is installed
|
||||
* Optional but recommended: Laravel Valet
|
||||
|
@ -6,7 +6,7 @@ Generally speaking, only the latest version of **PHP Monitor** is supported, exc
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Recommended Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 7.0 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 7.1 | ✅ Universal binary | ✅ Yes | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0+)<br/>Sequoia (15.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.5 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
||||
## Legacy versions
|
||||
|
||||
@ -14,6 +14,7 @@ These versions of PHP Monitor are no longer supported, but if you’re using an
|
||||
|
||||
| Version | Apple Silicon | Supported | Supported macOS | Minimum Deployment | Detected PHP Versions | Minimum Required Valet Version |
|
||||
| ------- | ------------- | ------------------ | ----- | ----- | ----- | ----
|
||||
| 7.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.2 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.1 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+)<br/>Sonoma (14.0) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.4 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.4 (w/ Valet 4.x)| 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
| 6.0 | ✅ Universal binary | ❌ | Monterey (12.4+)<br/>Ventura (13.0+) | macOS 12.4+ | PHP 5.6—PHP 8.2 (w/ Valet 2.x)<br/>PHP 7.0—PHP 8.2 (w/ Valet 3.x)<br/>PHP 7.1-PHP 8.2 (w/ Valet 4.x) | 3.0 or higher recommended<br/> 2.16.2 minimum |
|
||||
|
@ -24,13 +24,13 @@ struct Constants {
|
||||
|
||||
This hardcoded list will expire and will need to be modified when
|
||||
the cutoff date occurs, which is when the `php` formula will
|
||||
become PHP 8.4, and a new build will need to be made.
|
||||
become PHP 8.5, and a new build will need to be made.
|
||||
|
||||
If users launch an older version of the app, then a warning
|
||||
will be displayed to let them know that certain operations
|
||||
will not work correctly and that they need to update their app.
|
||||
*/
|
||||
static let PhpFormulaeCutoffDate = "2024-11-30" // YYYY-MM-DD
|
||||
static let PhpFormulaeCutoffDate = "2025-11-30" // YYYY-MM-DD
|
||||
|
||||
/**
|
||||
* The PHP versions that are considered pre-release versions.
|
||||
@ -39,8 +39,8 @@ struct Constants {
|
||||
*/
|
||||
static var ExperimentalPhpVersions: Set<String> {
|
||||
let releaseDates = [
|
||||
// "8.5": Date.fromString("2025-11-01"), // PLACEHOLDER DATE
|
||||
"8.4": Date.fromString("2024-11-22"),
|
||||
"8.5": Date.fromString(Self.PhpFormulaeCutoffDate),
|
||||
"8.4": Date.fromString("2024-11-22")
|
||||
]
|
||||
|
||||
return Set(releaseDates
|
||||
@ -73,7 +73,8 @@ struct Constants {
|
||||
static let DetectedPhpVersions: Set = [
|
||||
"5.6",
|
||||
"7.0", "7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4",
|
||||
"8.5" // DEV
|
||||
]
|
||||
|
||||
/**
|
||||
@ -94,7 +95,8 @@ struct Constants {
|
||||
4: // Valet v4 dropped support for v7.0
|
||||
[
|
||||
"7.1", "7.2", "7.3", "7.4",
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4"
|
||||
"8.0", "8.1", "8.2", "8.3", "8.4",
|
||||
"8.5" // DEV
|
||||
]
|
||||
]
|
||||
|
||||
@ -110,6 +112,14 @@ struct Constants {
|
||||
string: "https://phpmon.app/faq"
|
||||
)!
|
||||
|
||||
static let WikiPhpUnavailable = URL(
|
||||
string: "https://phpmon.app/php-unavailable"
|
||||
)!
|
||||
|
||||
static let WikiPhpUpgrade = URL(
|
||||
string: "https://phpmon.app/php-upgrade"
|
||||
)!
|
||||
|
||||
static let DonationPayment = URL(
|
||||
string: "https://phpmon.app/sponsor/now"
|
||||
)!
|
||||
|
@ -14,6 +14,8 @@ class PhpInstallation {
|
||||
|
||||
var iniFiles: [PhpConfigurationFile] = []
|
||||
|
||||
var isPreRelease: Bool = false
|
||||
|
||||
var isMissingBinary: Bool = false
|
||||
|
||||
var isHealthy: Bool = true
|
||||
@ -59,6 +61,10 @@ class PhpInstallation {
|
||||
trimNewlines: false
|
||||
).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if longVersionString.contains("-dev") {
|
||||
isPreRelease = true
|
||||
}
|
||||
|
||||
// The parser should always work, or the string has to be very unusual.
|
||||
// If so, the app SHOULD crash, so that the users report what's up.
|
||||
versionNumber = try! VersionNumber.parse(longVersionString)
|
||||
|
@ -18,11 +18,11 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
self.files = files
|
||||
|
||||
// Ensure that each of the ~ characters are replaced with the home directory path
|
||||
for key in self.files.keys where key.contains("~") {
|
||||
self.files.renameKey(
|
||||
fromKey: key,
|
||||
toKey: key.replacingOccurrences(of: "~", with: self.homeDirectory)
|
||||
)
|
||||
accessQueue.sync {
|
||||
for (key, value) in files {
|
||||
let adjustedKey = key.contains("~") ? key.replacingOccurrences(of: "~", with: self.homeDirectory) : key
|
||||
self.files[adjustedKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that intermediate directories are created
|
||||
@ -46,38 +46,49 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
*/
|
||||
private(set) var homeDirectory = "/Users/fake"
|
||||
|
||||
/**
|
||||
Serial dispatch queue for ensuring thread-safe access to the `files` dictionary.
|
||||
*/
|
||||
private let accessQueue = DispatchQueue(label: "com.testablefilesystem.accessQueue")
|
||||
|
||||
// MARK: - Basics
|
||||
|
||||
func createDirectory(_ path: String, withIntermediateDirectories: Bool) throws {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
try accessQueue.sync {
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
}
|
||||
|
||||
self.createIntermediateDirectories(path)
|
||||
|
||||
self.files[path] = .fake(.directory)
|
||||
}
|
||||
|
||||
self.createIntermediateDirectories(path)
|
||||
|
||||
self.files[path] = .fake(.directory)
|
||||
}
|
||||
|
||||
func writeAtomicallyToFile(_ path: String, content: String) throws {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
}
|
||||
try accessQueue.sync {
|
||||
if files[path] != nil {
|
||||
throw TestableFileSystemError.alreadyExists
|
||||
}
|
||||
|
||||
self.files[path] = .fake(.text, content)
|
||||
self.files[path] = .fake(.text, content)
|
||||
}
|
||||
}
|
||||
|
||||
func getStringFromFile(_ path: String) throws -> String {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
return try accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
|
||||
return file.content ?? ""
|
||||
return file.content ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
func getShallowContentsOfDirectory(_ path: String) throws -> [String] {
|
||||
@ -88,32 +99,36 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
seek = "\(seek)/"
|
||||
}
|
||||
|
||||
return self.files.keys
|
||||
.filter { $0.hasPrefix(seek) }
|
||||
.map { $0.replacingOccurrences(of: seek, with: "") }
|
||||
.filter { !$0.contains("/") }
|
||||
return accessQueue.sync {
|
||||
self.files.keys
|
||||
.filter { $0.hasPrefix(seek) }
|
||||
.map { $0.replacingOccurrences(of: seek, with: "") }
|
||||
.filter { !$0.contains("/") }
|
||||
}
|
||||
}
|
||||
|
||||
func getDestinationOfSymlink(_ path: String) throws -> String {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
return try accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
|
||||
if file.type != .symlink {
|
||||
throw TestableFileSystemError.notSymlink
|
||||
}
|
||||
if file.type != .symlink {
|
||||
throw TestableFileSystemError.notSymlink
|
||||
}
|
||||
|
||||
guard let pathToSymlink = file.content else {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
guard let pathToSymlink = file.content else {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
|
||||
if !files.keys.contains(pathToSymlink) {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
if !files.keys.contains(pathToSymlink) {
|
||||
throw TestableFileSystemError.invalidSymlink
|
||||
}
|
||||
|
||||
return pathToSymlink
|
||||
return pathToSymlink
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Move & Delete Files
|
||||
@ -122,27 +137,31 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
let newPath = newPath.replacingTildeWithHomeDirectory
|
||||
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.renameKey(
|
||||
fromKey: key,
|
||||
toKey: key.replacingOccurrences(of: path, with: newPath)
|
||||
)
|
||||
accessQueue.sync {
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.renameKey(
|
||||
fromKey: key,
|
||||
toKey: key.replacingOccurrences(of: path, with: newPath)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files.renameKey(fromKey: path, toKey: newPath)
|
||||
self.files.renameKey(fromKey: path, toKey: newPath)
|
||||
}
|
||||
}
|
||||
|
||||
func remove(_ path: String) throws {
|
||||
// Remove recursively
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.removeValue(forKey: key)
|
||||
accessQueue.sync {
|
||||
// Remove recursively
|
||||
self.files.keys.forEach { key in
|
||||
if key.hasPrefix(path) {
|
||||
self.files.removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.files.removeValue(forKey: path)
|
||||
self.files.removeValue(forKey: path)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: — Attributes
|
||||
@ -150,11 +169,13 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
func makeExecutable(_ path: String) throws {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
try accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
throw TestableFileSystemError.fileMissing
|
||||
}
|
||||
|
||||
file.type = .binary
|
||||
file.type = .binary
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Checks
|
||||
@ -162,93 +183,107 @@ class TestableFileSystem: FileSystemProtocol {
|
||||
func isExecutableFile(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.type == .binary
|
||||
return file.type == .binary
|
||||
}
|
||||
}
|
||||
|
||||
func isWriteableFile(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path.replacingTildeWithHomeDirectory] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return !file.readOnly
|
||||
return !file.readOnly
|
||||
}
|
||||
}
|
||||
|
||||
func anyExists(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
return files.keys.contains(path)
|
||||
return accessQueue.sync {
|
||||
files.keys.contains(path)
|
||||
}
|
||||
}
|
||||
|
||||
func fileExists(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return [.binary, .symlink, .text].contains(file.type)
|
||||
return [.binary, .symlink, .text].contains(file.type)
|
||||
}
|
||||
}
|
||||
|
||||
func directoryExists(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return [.directory].contains(file.type)
|
||||
return [.directory].contains(file.type)
|
||||
}
|
||||
}
|
||||
|
||||
func isSymlink(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.type == .symlink
|
||||
return file.type == .symlink
|
||||
}
|
||||
}
|
||||
|
||||
func isDirectory(_ path: String) -> Bool {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
return accessQueue.sync {
|
||||
guard let file = files[path] else {
|
||||
return false
|
||||
}
|
||||
|
||||
return file.type == .directory
|
||||
return file.type == .directory
|
||||
}
|
||||
}
|
||||
|
||||
public func printContents() {
|
||||
for key in self.files.keys.sorted() {
|
||||
print("\(key) -> \(self.files[key]!.type)")
|
||||
accessQueue.sync {
|
||||
for key in self.files.keys.sorted() {
|
||||
print("\(key) -> \(self.files[key]!.type)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func createIntermediateDirectories(_ path: String) {
|
||||
let path = path.replacingTildeWithHomeDirectory
|
||||
|
||||
let items = path.components(separatedBy: "/")
|
||||
|
||||
var preceding = ""
|
||||
|
||||
var directoriesToCreate: [String] = []
|
||||
|
||||
for item in items {
|
||||
let key = preceding == "/"
|
||||
? "/\(item)"
|
||||
: "\(preceding)/\(item)"
|
||||
|
||||
if !self.files.keys.contains(key) {
|
||||
self.files[key] = .fake(.directory)
|
||||
}
|
||||
|
||||
let key = preceding == "/" ? "/\(item)" : "\(preceding)/\(item)"
|
||||
directoriesToCreate.append(key)
|
||||
preceding = key
|
||||
}
|
||||
|
||||
for key in directoriesToCreate where !self.files.keys.contains(key) {
|
||||
self.files[key] = .fake(.directory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,20 @@ import NVAlert
|
||||
|
||||
class Startup {
|
||||
|
||||
@MainActor static var startupTimer: Timer?
|
||||
|
||||
@MainActor func startTimeoutTimer() {
|
||||
Self.startupTimer = Timer.scheduledTimer(
|
||||
timeInterval: 30.0, target: self, selector: #selector(startupTimeout),
|
||||
userInfo: nil, repeats: false
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor static func invalidateTimeoutTimer() {
|
||||
Self.startupTimer?.invalidate()
|
||||
Self.startupTimer = nil
|
||||
}
|
||||
|
||||
/**
|
||||
Checks the user's environment and checks if PHP Monitor can be used properly.
|
||||
This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more.
|
||||
@ -22,6 +36,11 @@ class Startup {
|
||||
// Do the important system setup checks
|
||||
Log.info("The user is running PHP Monitor with the architecture: \(App.architecture)")
|
||||
|
||||
// Set up a "background" timer on the main thread
|
||||
Task { @MainActor in
|
||||
startTimeoutTimer()
|
||||
}
|
||||
|
||||
for group in self.groups {
|
||||
if group.condition() {
|
||||
Log.info("Now running \(group.checks.count) \(group.name) checks!")
|
||||
@ -45,10 +64,34 @@ class Startup {
|
||||
// If we get here, nothing has gone wrong. That's what we want!
|
||||
initializeSwitcher()
|
||||
Log.info("PHP Monitor has determined the application has successfully passed all checks.")
|
||||
|
||||
Log.separator(as: .info)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
Displays an alert for when the application startup process takes too long.
|
||||
*/
|
||||
@MainActor @objc func startupTimeout() {
|
||||
NVAlert()
|
||||
.withInformation(
|
||||
title: "startup.timeout.title".localized,
|
||||
subtitle: "startup.timeout.subtitle".localized,
|
||||
description: "startup.timeout.description".localized
|
||||
)
|
||||
.withPrimary(text: "alert.cannot_start.close".localized, action: { vc in
|
||||
vc.close(with: .alertFirstButtonReturn)
|
||||
exit(1)
|
||||
})
|
||||
.withSecondary(text: "startup.timeout.ignore".localized, action: { vc in
|
||||
vc.close(with: .alertSecondButtonReturn)
|
||||
})
|
||||
.withTertiary(text: "", action: { _ in
|
||||
NSWorkspace.shared.open(URL(string: "https://github.com/nicoverbruggen/phpmon/issues/294")!)
|
||||
})
|
||||
.show()
|
||||
}
|
||||
|
||||
/**
|
||||
Displays an alert for a particular check. There are two types of alerts:
|
||||
- ones that require an app restart, which prompt the user to exit the app
|
||||
|
@ -35,10 +35,11 @@ class Brew {
|
||||
|
||||
/// Each formula for each PHP version that can be installed.
|
||||
public static let phpVersionFormulae = [
|
||||
"8.5": "shivammathur/php/php@8.5",
|
||||
"8.4": "shivammathur/php/php@8.4",
|
||||
"8.3": "php@8.3",
|
||||
"8.2": "php@8.2",
|
||||
"8.1": "php@8.1",
|
||||
"8.3": "shivammathur/php/php@8.3",
|
||||
"8.2": "shivammathur/php/php@8.2",
|
||||
"8.1": "shivammathur/php/php@8.1",
|
||||
"8.0": "shivammathur/php/php@8.0",
|
||||
"7.4": "shivammathur/php/php@7.4",
|
||||
"7.3": "shivammathur/php/php@7.3",
|
||||
|
@ -21,11 +21,12 @@ struct BrewPhpFormula: Equatable {
|
||||
/// The upgrade that is currently available, if it exists.
|
||||
let upgradeVersion: String?
|
||||
|
||||
// TODO: A rebuild attribute could be checked, to check if a Tap update exists for a pre-release version
|
||||
|
||||
/// Whether this formula is a stable version of PHP.
|
||||
let prerelease: Bool
|
||||
|
||||
/// Whether this formula's associated Homebrew file exists.
|
||||
var hasFormulaFile: Bool = false
|
||||
|
||||
/// Whether the formula is currently installed.
|
||||
var isInstalled: Bool {
|
||||
return installedVersion != nil
|
||||
@ -43,6 +44,7 @@ struct BrewPhpFormula: Equatable {
|
||||
self.installedVersion = installedVersion
|
||||
self.upgradeVersion = upgradeVersion
|
||||
self.prerelease = prerelease
|
||||
self.hasFormulaFile = checkFormulaFile()
|
||||
}
|
||||
|
||||
/// Whether the formula can be upgraded.
|
||||
@ -93,6 +95,21 @@ struct BrewPhpFormula: Equatable {
|
||||
return isHealthy() ?? true
|
||||
}
|
||||
|
||||
/**
|
||||
Verify whether the formula file exists (sourced via `shivammathur/homebrew-php`).
|
||||
If it does not exist, the formula cannot be installed.
|
||||
*/
|
||||
private func checkFormulaFile() -> Bool {
|
||||
guard let version = shortVersion else {
|
||||
return false
|
||||
}
|
||||
|
||||
return FileSystem.fileExists(
|
||||
"\(Paths.tapPath)/shivammathur/homebrew-php/Formula/php@\(version).rb"
|
||||
.replacingOccurrences(of: "php@" + PhpEnvironments.brewPhpAlias, with: "php")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if this PHP installation is healthy.
|
||||
* Uses the cached installation health check as basis.
|
||||
|
@ -39,19 +39,25 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||
OutdatedFormulae.self,
|
||||
from: rawJsonText
|
||||
).formulae.filter({ formula in
|
||||
formula.name.starts(with: "php")
|
||||
formula.name.starts(with: "shivammathur/php/php") || formula.name.starts(with: "php")
|
||||
})
|
||||
}
|
||||
|
||||
return Brew.phpVersionFormulae.map { (version, formula) in
|
||||
var fullVersion: String?
|
||||
var upgradeVersion: String?
|
||||
var isPrerelease: Bool = Constants.ExperimentalPhpVersions.contains(version)
|
||||
|
||||
if let install = PhpEnvironments.shared.cachedPhpInstallations[version] {
|
||||
fullVersion = install.versionNumber.text
|
||||
fullVersion = install.isPreRelease ? "\(fullVersion!)-dev" : fullVersion
|
||||
|
||||
upgradeVersion = outdated?.first(where: { formula in
|
||||
return formula.name == install.formulaName
|
||||
return formula.name.replacingOccurrences(of: "shivammathur/php/", with: "")
|
||||
== install.formulaName.replacingOccurrences(of: "shivammathur/php/", with: "")
|
||||
})?.current_version
|
||||
|
||||
isPrerelease = install.isPreRelease
|
||||
}
|
||||
|
||||
return BrewPhpFormula(
|
||||
@ -59,7 +65,7 @@ class BrewPhpFormulaeHandler: HandlesBrewPhpFormulae {
|
||||
displayName: "PHP \(version)",
|
||||
installedVersion: fullVersion,
|
||||
upgradeVersion: upgradeVersion,
|
||||
prerelease: Constants.ExperimentalPhpVersions.contains(version)
|
||||
prerelease: isPrerelease
|
||||
)
|
||||
}.sorted { $0.displayName > $1.displayName }
|
||||
}
|
||||
|
@ -115,6 +115,9 @@ extension MainMenu {
|
||||
// Finally!
|
||||
Log.info("PHP Monitor is ready to serve!")
|
||||
|
||||
// Avoid showing the "startup timeout" alert
|
||||
Startup.invalidateTimeoutTimer()
|
||||
|
||||
// Check if we upgraded from a previous version
|
||||
AppUpdater.checkIfUpdateWasPerformed()
|
||||
}
|
||||
|
@ -10,6 +10,11 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct HelpButton: View {
|
||||
@State var frameSize: CGFloat = 14
|
||||
@State var textSize: CGFloat = 12
|
||||
@State var shadowOpacity: CGFloat = 0.3
|
||||
@State var shadowRadius: CGFloat = 1
|
||||
|
||||
var action: () -> Void
|
||||
|
||||
var body: some View {
|
||||
@ -18,9 +23,10 @@ struct HelpButton: View {
|
||||
Circle()
|
||||
.strokeBorder(Color(NSColor.separatorColor), lineWidth: 0.5)
|
||||
.background(Circle().foregroundColor(Color(NSColor.controlColor)).opacity(0.7))
|
||||
.shadow(color: Color(NSColor.separatorColor).opacity(0.3), radius: 1)
|
||||
.frame(width: 14, height: 14)
|
||||
Text("?").font(.system(size: 12, weight: .medium))
|
||||
.shadow(color: Color(NSColor.separatorColor)
|
||||
.opacity(shadowOpacity), radius: shadowRadius)
|
||||
.frame(width: frameSize, height: frameSize)
|
||||
Text("?").font(.system(size: textSize, weight: .medium))
|
||||
.foregroundColor(Color(NSColor.labelColor))
|
||||
}
|
||||
})
|
||||
|
@ -112,5 +112,4 @@ struct ByteLimitView: View {
|
||||
#Preview("Config Manager") {
|
||||
ConfigManagerView()
|
||||
.frame(width: 600, height: .infinity)
|
||||
.previewDisplayName("Config Manager")
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
struct PhpVersionManagerView: View {
|
||||
@ObservedObject var formulae: BrewFormulaeObservable
|
||||
@ObservedObject var status: BusyStatus
|
||||
@ -192,6 +193,7 @@ struct PhpVersionManagerView: View {
|
||||
.buttonStyle(.automatic)
|
||||
.controlSize(.large)
|
||||
}
|
||||
.accessibilityIdentifier("RefreshButton")
|
||||
.focusable(false)
|
||||
.disabled(self.status.busy)
|
||||
|
||||
@ -231,14 +233,18 @@ struct PhpVersionManagerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if formula.isInstalled {
|
||||
if formula.hasUpgradedFormulaAlias {
|
||||
HelpButton(frameSize: 18, textSize: 14, shadowOpacity: 1, shadowRadius: 2, action: {
|
||||
NSWorkspace.shared.open(Constants.Urls.WikiPhpUpgrade)
|
||||
})
|
||||
} else if formula.isInstalled {
|
||||
Button("phpman.buttons.uninstall".localizedForSwiftUI, role: .destructive) {
|
||||
Task { await self.confirmUninstall(formula) }
|
||||
}
|
||||
} else {
|
||||
Button("phpman.buttons.install".localizedForSwiftUI) {
|
||||
Task { await self.install(formula) }
|
||||
}.disabled(formula.hasUpgradedFormulaAlias)
|
||||
}.disabled(formula.hasUpgradedFormulaAlias || !formula.hasFormulaFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -268,6 +274,8 @@ struct PhpVersionManagerView: View {
|
||||
Text("phpman.version.installed".localized(formula.installedVersion!))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.gray)
|
||||
} else if !formula.hasFormulaFile {
|
||||
unavailableFormula()
|
||||
} else {
|
||||
Text("phpman.version.available_for_installation".localizedForSwiftUI)
|
||||
.font(.system(size: 11))
|
||||
@ -291,7 +299,19 @@ struct PhpVersionManagerView: View {
|
||||
.foregroundColor(formula.iconColor)
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
|
||||
private func unavailableFormula() -> some View {
|
||||
HStack(spacing: 5) {
|
||||
Text("phpman.version.unavailable".localizedForSwiftUI)
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(.gray)
|
||||
HelpButton(action: {
|
||||
NSWorkspace.shared.open(Constants.Urls.WikiPhpUnavailable)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// swiftlint:enable type_body_length
|
||||
|
||||
#Preview {
|
||||
PhpVersionManagerView(
|
||||
|
@ -45,7 +45,7 @@ extension NSEvent.ModifierFlags {
|
||||
}
|
||||
}
|
||||
|
||||
extension NSEvent.ModifierFlags: CustomStringConvertible {
|
||||
extension NSEvent.ModifierFlags: @retroactive CustomStringConvertible {
|
||||
public var description: String {
|
||||
var output = ""
|
||||
|
||||
|
@ -138,7 +138,9 @@ and restart PHP Monitor for extensions to become visible. If the problem persist
|
||||
"phpman.version.has_update" = "Version %@ installed, %@ available.";
|
||||
"phpman.version.installed" = "Version %@ is currently installed.";
|
||||
"phpman.version.available_for_installation" = "This version can be installed.";
|
||||
"phpman.version.automatic_upgrade" = "This version will be automatically installed by upgrading an older version.";
|
||||
"phpman.version.unavailable" = "This version is temporarily unavailable.";
|
||||
"phpman.version.automatic_upgrade" = "This version will be automatically installed by upgrading an older version.
|
||||
(Both this new version and the old one will be available after upgrading.)";
|
||||
"phpman.buttons.uninstall" = "Uninstall";
|
||||
"phpman.buttons.install" = "Install";
|
||||
"phpman.buttons.update" = "Update";
|
||||
@ -864,3 +866,14 @@ Please note that some features (greyed out below) are currently unavailable beca
|
||||
|
||||
"alert.language_changed.title" = "You must restart PHP Monitor!";
|
||||
"alert.language_changed.subtitle" = "You just changed the display language of PHP Monitor. The menu will immediately use the correct language, but you may need to restart the app for all text throughout the app to reflect your new language choice.";
|
||||
|
||||
// STARTUP TIMEOUT
|
||||
|
||||
"startup.timeout.ignore" = "Ignore";
|
||||
"startup.timeout.title" = "PHP Monitor is taking too long to initialize!";
|
||||
"startup.timeout.subtitle" = "If PHP Monitor remains busy for longer than 30 seconds, there may be something wrong with your Homebrew setup.";
|
||||
"startup.timeout.description" = "Sometimes, due to various file permission issues, things may break. You can try using `brew doctor` and `brew cleanup` to fix this.
|
||||
|
||||
It is recommended to restart PHP Monitor afterwards. Learn more about this issue at: https://github.com/nicoverbruggen/phpmon/issues/294.
|
||||
|
||||
If PHP Monitor has finished initializing anyway or you want to wait a bit longer, feel free to click 'Ignore' and use PHP Monitor as usual. Either way, you may want to investigate, because this isn't supposed to take this long.";
|
||||
|
@ -35,6 +35,19 @@ class TestableConfigurations {
|
||||
"loopback": "127.0.0.1"
|
||||
}
|
||||
"""),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php.rb" : .fake(.text),
|
||||
// "/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.5.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.4.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.3.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.2.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.1.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@8.0.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.4.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.3.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.2.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.1.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@7.0.rb" : .fake(.text),
|
||||
"/opt/homebrew/Library/Taps/shivammathur/homebrew-php/Formula/php@5.6.rb" : .fake(.text)
|
||||
],
|
||||
shellOutput: [
|
||||
"/opt/homebrew/bin/brew --version"
|
||||
|
@ -85,22 +85,27 @@ final class MainMenuTest: UITestCase {
|
||||
|
||||
final func test_can_open_php_version_manager() throws {
|
||||
let app = launch(openMenu: true)
|
||||
|
||||
app.mainMenuItem(withText: "mi_php_version_manager".localized).click()
|
||||
|
||||
// Should display loader
|
||||
assertExists(app.staticTexts["phpman.busy.title".localized], 1)
|
||||
|
||||
// After loading, should display PHP 8.2 and PHP 8.3
|
||||
// After loading, should display PHP 8.2, PHP 8.3, PHP 8.4
|
||||
assertExists(app.staticTexts["PHP 8.2"], 5)
|
||||
assertExists(app.staticTexts["PHP 8.3"])
|
||||
assertExists(app.staticTexts["PHP 8.4"])
|
||||
|
||||
// Should also display pre-release version
|
||||
assertExists(app.staticTexts["PHP 8.4"])
|
||||
assertExists(app.staticTexts["PHP 8.5"])
|
||||
assertExists(app.staticTexts["phpman.version.prerelease".localized.uppercased()])
|
||||
assertExists(app.staticTexts["phpman.version.available_for_installation".localized])
|
||||
|
||||
// But not PHP 8.5 (yet)
|
||||
assertNotExists(app.staticTexts["PHP 8.5"])
|
||||
// The pre-release version should be unavailable
|
||||
assertExists(app.staticTexts["phpman.version.unavailable".localized])
|
||||
|
||||
// But not PHP 8.6 (yet)
|
||||
assertNotExists(app.staticTexts["PHP 8.6"])
|
||||
|
||||
// Also, PHP 8.2 should have an update available
|
||||
assertExists(app.staticTexts["phpman.version.has_update".localized(
|
||||
|
Reference in New Issue
Block a user