From 99881bf4cd8eb1ac015227c86e6c1590e8e2b4c0 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 4 May 2022 20:25:59 +0200 Subject: [PATCH 01/58] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20WIP:=20Refactor=20de?= =?UTF-8?q?termining=20PHP=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The way .ini files are loaded is changing with this commit. Instead of directly saving which extensions were found, the extensions loaded are now determined by reading the .ini file. However, there are some performance concerns here. Perhaps it is worth *not* reloading the contents of these files unless absolutely necessary. --- PHP Monitor.xcodeproj/project.pbxproj | 10 +++++ phpmon-tests/Parsers/PhpIniTest.swift | 23 +++++++++++ phpmon/Common/PHP/ActivePhpInstallation.swift | 30 +++++++++++---- phpmon/Common/PHP/PhpInitializationFile.swift | 38 +++++++++++++++++++ 4 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 phpmon-tests/Parsers/PhpIniTest.swift create mode 100644 phpmon/Common/PHP/PhpInitializationFile.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 320449b..185f08e 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -118,6 +118,9 @@ C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; }; C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */; }; C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; + C46FA9882822EFDC00D78807 /* PhpInitializationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */; }; + C46FA9892822EFDC00D78807 /* PhpInitializationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */; }; + C46FA98C2822F08F00D78807 /* PhpIniTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA98A2822F08F00D78807 /* PhpIniTest.swift */; }; C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; }; @@ -338,6 +341,8 @@ C464ADAE275A7A69003FCD53 /* DomainListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListVC.swift; sourceTree = ""; }; C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListCellProtocol.swift; sourceTree = ""; }; C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; + C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInitializationFile.swift; sourceTree = ""; }; + C46FA98A2822F08F00D78807 /* PhpIniTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpIniTest.swift; sourceTree = ""; }; C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = ""; }; C474B00524C0E98C00066A22 /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = ""; }; @@ -461,6 +466,7 @@ C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */, C4F2E4392752F7D00020E974 /* PhpInstallation.swift */, C4ACA38E25C754C100060C66 /* PhpExtension.swift */, + C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */, ); path = PHP; sourceTree = ""; @@ -854,6 +860,7 @@ children = ( C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */, C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */, + C46FA98A2822F08F00D78807 /* PhpIniTest.swift */, C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */, C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */, ); @@ -1125,6 +1132,7 @@ C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C4B585412770FE3900DA4FBE /* Shell.swift in Sources */, C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */, + C46FA9882822EFDC00D78807 /* PhpInitializationFile.swift in Sources */, C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */, @@ -1271,6 +1279,7 @@ C4C0E8E327F88B13002D32A9 /* ValetSiteScanner.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, + C46FA98C2822F08F00D78807 /* PhpIniTest.swift in Sources */, C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */, C4C0E8EB27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */, C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, @@ -1331,6 +1340,7 @@ C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */, C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */, C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, + C46FA9892822EFDC00D78807 /* PhpInitializationFile.swift in Sources */, C41C02AB27E61CB3009F26CB /* ValetSite+Fake.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */, diff --git a/phpmon-tests/Parsers/PhpIniTest.swift b/phpmon-tests/Parsers/PhpIniTest.swift new file mode 100644 index 0000000..d6f20b6 --- /dev/null +++ b/phpmon-tests/Parsers/PhpIniTest.swift @@ -0,0 +1,23 @@ +// +// PhpIniTest.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 04/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import XCTest + +class PhpIniTest: XCTestCase { + + static var phpIniFileUrl: URL { + return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")! + } + + func testCanLoadExtension() throws { + let iniFile = PhpInitializationFile(fileUrl: Self.phpIniFileUrl) + + XCTAssertNotNil(iniFile) + } + +} diff --git a/phpmon/Common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift index cc462ad..4eb125d 100644 --- a/phpmon/Common/PHP/ActivePhpInstallation.swift +++ b/phpmon/Common/PHP/ActivePhpInstallation.swift @@ -20,7 +20,13 @@ class ActivePhpInstallation { var version: Version! var limits: Limits! - var extensions: [PhpExtension]! + var iniFiles: [PhpInitializationFile] = [] + + var extensions: [PhpExtension] { + return iniFiles.flatMap { initFile in + return initFile.extensions + } + } // MARK: - Computed @@ -34,16 +40,23 @@ class ActivePhpInstallation { // Show information about the current version getVersion() + // Initialize the list of ini files that are loaded + iniFiles = [] + // If an error occurred, exit early if version.error { limits = Limits() - extensions = [] return } // Load extension information - let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") - extensions = PhpExtension.load(from: path) + let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") + + iniFiles.append( + PhpInitializationFile(fileUrl: mainConfigurationFileUrl) + ) + + // extensions.append(contentsOf: PhpExtension.load(from: mainConfigurationFileUrl)) // Get configuration values limits = Limits( @@ -60,10 +73,11 @@ class ActivePhpInstallation { // See if any extensions are present in said .ini files paths.forEach { (iniFilePath) in - let loadedExtensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath)) - if loadedExtensions.isEmpty { - extensions.append(contentsOf: loadedExtensions) - } + let fileUrl = URL(fileURLWithPath: iniFilePath) + + iniFiles.append( + PhpInitializationFile(fileUrl: fileUrl) + ) } } diff --git a/phpmon/Common/PHP/PhpInitializationFile.swift b/phpmon/Common/PHP/PhpInitializationFile.swift new file mode 100644 index 0000000..c731941 --- /dev/null +++ b/phpmon/Common/PHP/PhpInitializationFile.swift @@ -0,0 +1,38 @@ +// +// PhpInitFile.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 04/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class PhpInitializationFile { + + /// The file where this extension was located. + let file: String + + /// The original string contained within the file when scanned. + let raw: [String] + + /// The extensions found in this .ini file. + let extensions: [PhpExtension] + + init(fileUrl: URL) { + self.file = fileUrl.path + + let rawString = (try? String(contentsOf: fileUrl, encoding: .utf8)) ?? "" + + self.raw = rawString.components(separatedBy: "\n") + + self.extensions = PhpExtension.load(from: fileUrl) + + dump(self) + + // TODO: Actually parse the .ini file + // Parsing the file could be done like this gist: + // https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553 + } + +} From f725e09f5503bb17eccd7e305fb0f60d2430a9d9 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 5 May 2022 20:05:52 +0200 Subject: [PATCH 02/58] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20WIP:=20Parsing=20log?= =?UTF-8?q?ic=20for=20configuration=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 12 +- phpmon/Common/PHP/ActivePhpInstallation.swift | 6 +- phpmon/Common/PHP/PhpConfigurationFile.swift | 128 ++++++++++++++++++ phpmon/Common/PHP/PhpInitializationFile.swift | 38 ------ 4 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 phpmon/Common/PHP/PhpConfigurationFile.swift delete mode 100644 phpmon/Common/PHP/PhpInitializationFile.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 185f08e..4c53cfb 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -118,8 +118,8 @@ C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; }; C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */; }; C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; - C46FA9882822EFDC00D78807 /* PhpInitializationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */; }; - C46FA9892822EFDC00D78807 /* PhpInitializationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */; }; + C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; }; + C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; }; C46FA98C2822F08F00D78807 /* PhpIniTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA98A2822F08F00D78807 /* PhpIniTest.swift */; }; C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; @@ -341,7 +341,7 @@ C464ADAE275A7A69003FCD53 /* DomainListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListVC.swift; sourceTree = ""; }; C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListCellProtocol.swift; sourceTree = ""; }; C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; - C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInitializationFile.swift; sourceTree = ""; }; + C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationFile.swift; sourceTree = ""; }; C46FA98A2822F08F00D78807 /* PhpIniTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpIniTest.swift; sourceTree = ""; }; C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = ""; }; @@ -466,7 +466,7 @@ C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */, C4F2E4392752F7D00020E974 /* PhpInstallation.swift */, C4ACA38E25C754C100060C66 /* PhpExtension.swift */, - C46FA9872822EFDC00D78807 /* PhpInitializationFile.swift */, + C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */, ); path = PHP; sourceTree = ""; @@ -1132,7 +1132,7 @@ C48D6C70279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, C4B585412770FE3900DA4FBE /* Shell.swift in Sources */, C4998F0A2617633900B2526E /* PrefsWC.swift in Sources */, - C46FA9882822EFDC00D78807 /* PhpInitializationFile.swift in Sources */, + C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */, C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */, C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */, @@ -1340,7 +1340,7 @@ C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */, C449B4F327EE7FC600C47E8A /* DomainListTypeCell.swift in Sources */, C48D6C71279CD2AC00F26D7E /* PhpVersionNumber.swift in Sources */, - C46FA9892822EFDC00D78807 /* PhpInitializationFile.swift in Sources */, + C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */, C41C02AB27E61CB3009F26CB /* ValetSite+Fake.swift in Sources */, C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */, C4D9F24C280B69E100DCD39A /* AddProxyVC.swift in Sources */, diff --git a/phpmon/Common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift index 4eb125d..a423fbf 100644 --- a/phpmon/Common/PHP/ActivePhpInstallation.swift +++ b/phpmon/Common/PHP/ActivePhpInstallation.swift @@ -20,7 +20,7 @@ class ActivePhpInstallation { var version: Version! var limits: Limits! - var iniFiles: [PhpInitializationFile] = [] + var iniFiles: [PhpConfigurationFile] = [] var extensions: [PhpExtension] { return iniFiles.flatMap { initFile in @@ -53,7 +53,7 @@ class ActivePhpInstallation { let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") iniFiles.append( - PhpInitializationFile(fileUrl: mainConfigurationFileUrl) + PhpConfigurationFile(fileUrl: mainConfigurationFileUrl) ) // extensions.append(contentsOf: PhpExtension.load(from: mainConfigurationFileUrl)) @@ -76,7 +76,7 @@ class ActivePhpInstallation { let fileUrl = URL(fileURLWithPath: iniFilePath) iniFiles.append( - PhpInitializationFile(fileUrl: fileUrl) + PhpConfigurationFile(fileUrl: fileUrl) ) } } diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift new file mode 100644 index 0000000..6a73250 --- /dev/null +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -0,0 +1,128 @@ +// +// PhpInitFile.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 04/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class PhpConfigurationFile { + + typealias Section = [String: String] + typealias Config = [String: Section] + + /// The file where this configuration file was located. + let file: String + + /// The extensions found in this .ini file. + let extensions: [PhpExtension] + + /// The actual content of the configuration file. + var content: Config + + init(fileUrl: URL) { + self.file = fileUrl.path + + let rawString = (try? String(contentsOf: fileUrl, encoding: .utf8)) ?? "" + + self.extensions = PhpExtension.load(from: fileUrl) + + self.content = Self.parseConfig(from: rawString.components(separatedBy: "\n")) + + dump(self) + } + + // MARK: Parsing Logic + // Slightly modified from: https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553 + + /** + Attempts to parse the configuration file, based on an array of strings. + Each string is a line from the configuration file. + */ + private static func parseConfig(from lines: [String]) -> Config { + var config = Config() + var currentSectionName = "main" + for line in lines { + let line = trim(line) + if line.hasPrefix("[") && line.hasSuffix("]") { + currentSectionName = parseSectionHeader(line) + } else if let (key, value) = parseLine(line) { + var section = config[currentSectionName] ?? [:] + section[key] = value + config[currentSectionName] = section + } + } + return config + } + + /** + Remove all whitespace and additional characters from individual lines. + */ + private static func trim(_ s: String) -> String { + let whitespaces = CharacterSet(charactersIn: " \n\r\t") + return s.trimmingCharacters(in: whitespaces) + } + + /** + It may prove beneficial to strip all comments, which can start with # or ;. + In this case, strip both. + */ + private static func stripComment(_ line: String) -> String { + var line = line + + let characters: [String.Element] = ["#", ";"] + + for character in characters { + // Only keep checking for comments as long as the line isn't empty + if line.isEmpty { + return line + } + + // Check for the next comment character + line = strip(character: character, line) + } + + return line + } + + /** + Empties a line if it happens to be commented out, causing it to be ignored. + */ + private static func strip(character: String.Element, _ line: String) -> String { + let parts = line.split( + separator: character, + maxSplits: 1, + omittingEmptySubsequences: false + ) + if !parts.isEmpty { + return String(parts[0]) + } + return "" + } + + /** + Attempts to parse a section header. Requires the line to start with [ and end with ]. + */ + private static func parseSectionHeader(_ line: String) -> String { + let from = line.index(after: line.startIndex) + let to = line.index(before: line.endIndex) + return line[from.. (String, String)? { + let parts = stripComment(line) + .split(separator: "=", maxSplits: 1) + if parts.count == 2 { + let k = trim(String(parts[0])) + let v = trim(String(parts[1])) + return (k, v) + } + return nil + } + +} diff --git a/phpmon/Common/PHP/PhpInitializationFile.swift b/phpmon/Common/PHP/PhpInitializationFile.swift deleted file mode 100644 index c731941..0000000 --- a/phpmon/Common/PHP/PhpInitializationFile.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// PhpInitFile.swift -// PHP Monitor -// -// Created by Nico Verbruggen on 04/05/2022. -// Copyright © 2022 Nico Verbruggen. All rights reserved. -// - -import Foundation - -class PhpInitializationFile { - - /// The file where this extension was located. - let file: String - - /// The original string contained within the file when scanned. - let raw: [String] - - /// The extensions found in this .ini file. - let extensions: [PhpExtension] - - init(fileUrl: URL) { - self.file = fileUrl.path - - let rawString = (try? String(contentsOf: fileUrl, encoding: .utf8)) ?? "" - - self.raw = rawString.components(separatedBy: "\n") - - self.extensions = PhpExtension.load(from: fileUrl) - - dump(self) - - // TODO: Actually parse the .ini file - // Parsing the file could be done like this gist: - // https://gist.github.com/jetmind/f776c0d223e4ac6aec1ff9389e874553 - } - -} From f679231ade77e496b29a4f77a8887494693f94a6 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 5 May 2022 20:09:40 +0200 Subject: [PATCH 03/58] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Common/PHP/PhpConfigurationFile.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift index 6a73250..c0ab7bb 100644 --- a/phpmon/Common/PHP/PhpConfigurationFile.swift +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -43,9 +43,12 @@ class PhpConfigurationFile { */ private static func parseConfig(from lines: [String]) -> Config { var config = Config() + var currentSectionName = "main" + for line in lines { let line = trim(line) + if line.hasPrefix("[") && line.hasSuffix("]") { currentSectionName = parseSectionHeader(line) } else if let (key, value) = parseLine(line) { @@ -54,15 +57,16 @@ class PhpConfigurationFile { config[currentSectionName] = section } } + return config } /** Remove all whitespace and additional characters from individual lines. */ - private static func trim(_ s: String) -> String { + private static func trim(_ string: String) -> String { let whitespaces = CharacterSet(charactersIn: " \n\r\t") - return s.trimmingCharacters(in: whitespaces) + return string.trimmingCharacters(in: whitespaces) } /** @@ -96,9 +100,11 @@ class PhpConfigurationFile { maxSplits: 1, omittingEmptySubsequences: false ) + if !parts.isEmpty { return String(parts[0]) } + return "" } @@ -108,6 +114,7 @@ class PhpConfigurationFile { private static func parseSectionHeader(_ line: String) -> String { let from = line.index(after: line.startIndex) let to = line.index(before: line.endIndex) + return line[from.. (String, String)? { let parts = stripComment(line) .split(separator: "=", maxSplits: 1) + if parts.count == 2 { let k = trim(String(parts[0])) let v = trim(String(parts[1])) return (k, v) } + return nil } From 1392b6e4a098a3d20be6cc9fe92071010759eca6 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Fri, 13 May 2022 17:04:45 +0200 Subject: [PATCH 04/58] =?UTF-8?q?=F0=9F=94=A7=20New=20version=20under=20de?= =?UTF-8?q?velopment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index e2f4086..94f94bc 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1530,7 +1530,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 786; + CURRENT_PROJECT_VERSION = 790; DEBUG = YES; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; @@ -1540,7 +1540,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 5.3; + MARKETING_VERSION = "5.4-dev"; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1556,7 +1556,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 786; + CURRENT_PROJECT_VERSION = 790; DEBUG = NO; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; @@ -1566,7 +1566,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 5.3; + MARKETING_VERSION = "5.4-dev"; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; From b0c62e226a2502cbe76f39ba86ec8424ed232fc3 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 15 May 2022 15:15:49 +0200 Subject: [PATCH 05/58] =?UTF-8?q?=F0=9F=91=8C=20Code=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 34 +++++++++++++------ .../Parsers/NginxConfigurationTest.swift | 16 ++++----- ...iTest.swift => PhpConfigurationTest.swift} | 4 +-- phpmon/Common/PHP/ActivePhpInstallation.swift | 16 ++++----- phpmon/Common/PHP/PhpConfigurationFile.swift | 29 ++++++++++++---- phpmon/Common/Protocols/CreatedFromFile.swift | 15 ++++++++ ...ion.swift => NginxConfigurationFile.swift} | 11 +++--- .../ProxyScanner/ValetProxyScanner.swift | 2 +- .../Valet/Proxies/ValetProxy.swift | 2 +- .../Integrations/Valet/Sites/ValetSite.swift | 2 +- 10 files changed, 87 insertions(+), 44 deletions(-) rename phpmon-tests/Parsers/{PhpIniTest.swift => PhpConfigurationTest.swift} (75%) create mode 100644 phpmon/Common/Protocols/CreatedFromFile.swift rename phpmon/Domain/Integrations/Nginx/{NginxConfiguration.swift => NginxConfigurationFile.swift} (91%) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 94f94bc..8c19794 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -9,6 +9,8 @@ /* Begin PBXBuildFile section */ 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; }; 5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; }; + 5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; }; + 5489625928313231004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; }; 54A18D40282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test in Resources */ = {isa = PBXBuildFile; fileRef = 54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */; }; 54B48B5F275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; }; 54B48B60275F66AE006D90C5 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B48B5E275F66AE006D90C5 /* Application.swift */; }; @@ -127,7 +129,7 @@ C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; C46FA9882822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; }; C46FA9892822EFDC00D78807 /* PhpConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */; }; - C46FA98C2822F08F00D78807 /* PhpIniTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA98A2822F08F00D78807 /* PhpIniTest.swift */; }; + C46FA98C2822F08F00D78807 /* PhpConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA98A2822F08F00D78807 /* PhpConfigurationTest.swift */; }; C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; }; @@ -199,8 +201,8 @@ C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; }; C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */; }; C4CE3BBC27B324250086CA49 /* ComposerWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */; }; - C4D5CFCA27E0F9CD00035329 /* NginxConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */; }; - C4D5CFCB27E0F9CD00035329 /* NginxConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */; }; + C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */; }; + C4D5CFCB27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */; }; C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; }; C4D89BC62783C99400A02B68 /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; C4D936C927E3EB6100BD69FE /* PhpHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D936C827E3EB6100BD69FE /* PhpHelper.swift */; }; @@ -279,6 +281,7 @@ /* Begin PBXFileReference section */ 5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = ""; }; 5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; + 5489625728312FAD004F647A /* CreatedFromFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatedFromFile.swift; sourceTree = ""; }; 54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy-custom-tld.test"; sourceTree = ""; }; 54B48B5E275F66AE006D90C5 /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; 54D9E0AC27E4F51E003B9AD9 /* HotKeysController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeysController.swift; sourceTree = ""; }; @@ -354,7 +357,7 @@ C46E206F2829D27F00D909D6 /* AppUpdaterCheckTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppUpdaterCheckTest.swift; sourceTree = ""; }; C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; C46FA9872822EFDC00D78807 /* PhpConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationFile.swift; sourceTree = ""; }; - C46FA98A2822F08F00D78807 /* PhpIniTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpIniTest.swift; sourceTree = ""; }; + C46FA98A2822F08F00D78807 /* PhpConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpConfigurationTest.swift; sourceTree = ""; }; C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = ""; }; C474B00524C0E98C00066A22 /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = ""; }; @@ -399,7 +402,7 @@ C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; }; C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Switcher.swift"; sourceTree = ""; }; C4CE3BB927B31F670086CA49 /* ComposerWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerWindow.swift; sourceTree = ""; }; - C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfiguration.swift; sourceTree = ""; }; + C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NginxConfigurationFile.swift; sourceTree = ""; }; C4D8016522B1584700C6DA1B /* Startup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Startup.swift; sourceTree = ""; }; C4D89BC52783C99400A02B68 /* ComposerJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerJson.swift; sourceTree = ""; }; C4D936C827E3EB6100BD69FE /* PhpHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpHelper.swift; sourceTree = ""; }; @@ -468,6 +471,14 @@ path = Preferences; sourceTree = ""; }; + 5489625628312F95004F647A /* Protocols */ = { + isa = PBXGroup; + children = ( + 5489625728312FAD004F647A /* CreatedFromFile.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 54B20EDF263AA22C00D3250E /* PHP */ = { isa = PBXGroup; children = ( @@ -819,6 +830,7 @@ C44CCD4327AFE93300CE40E5 /* Errors */, C4F8C0A222D4F100002EFE61 /* Extensions */, C4811D2822D70D9C00B5F6B3 /* Helpers */, + 5489625628312F95004F647A /* Protocols */, ); path = Common; sourceTree = ""; @@ -846,7 +858,7 @@ C4C0E8DA27F887CC002D32A9 /* Nginx */ = { isa = PBXGroup; children = ( - C4D5CFC927E0F9CD00035329 /* NginxConfiguration.swift */, + C4D5CFC927E0F9CD00035329 /* NginxConfigurationFile.swift */, ); path = Nginx; sourceTree = ""; @@ -875,7 +887,7 @@ children = ( C4AF9F76275447F100D44ED0 /* ValetConfigurationTest.swift */, C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */, - C46FA98A2822F08F00D78807 /* PhpIniTest.swift */, + C46FA98A2822F08F00D78807 /* PhpConfigurationTest.swift */, C43A8A2325D9D20D00591B77 /* HomebrewPackageTest.swift */, C42CFB1927DFE8BD00862737 /* NginxConfigurationTest.swift */, ); @@ -1156,6 +1168,7 @@ C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, + 5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */, C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, C4080FF627BD8C6400BF2C6B /* BetterAlert.swift in Sources */, C4E0F7ED27BEBDA9007475F2 /* NSWindowExtension.swift in Sources */, @@ -1232,7 +1245,7 @@ C476FF9822B0DD830098105B /* Alert.swift in Sources */, C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */, C48D0C9625CC80B100CC7490 /* HeaderView.swift in Sources */, - C4D5CFCA27E0F9CD00035329 /* NginxConfiguration.swift in Sources */, + C4D5CFCA27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */, C4CE3BBA27B31F670086CA49 /* ComposerWindow.swift in Sources */, C4D9ADC8277611A0007277F4 /* InternalSwitcher.swift in Sources */, C4080FFA27BD956700BF2C6B /* BetterAlertVC.swift in Sources */, @@ -1280,7 +1293,7 @@ C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */, 54D9E0BB27E4F51E003B9AD9 /* ModifierFlagsExtension.swift in Sources */, C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */, - C4D5CFCB27E0F9CD00035329 /* NginxConfiguration.swift in Sources */, + C4D5CFCB27E0F9CD00035329 /* NginxConfigurationFile.swift in Sources */, C4068CA827B07A1300544CD5 /* SelectPreferenceView.swift in Sources */, C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */, C40C7F2927721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, @@ -1299,7 +1312,7 @@ C4C0E8E327F88B13002D32A9 /* ValetSiteScanner.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, - C46FA98C2822F08F00D78807 /* PhpIniTest.swift in Sources */, + C46FA98C2822F08F00D78807 /* PhpConfigurationTest.swift in Sources */, C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */, C4C0E8EB27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */, C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, @@ -1327,6 +1340,7 @@ C417DC75277614690015E6EE /* Helpers.swift in Sources */, C4080FF727BD8C6400BF2C6B /* BetterAlert.swift in Sources */, C4B97B7C275CF20A003F3378 /* App+GlobalHotkey.swift in Sources */, + 5489625928313231004F647A /* CreatedFromFile.swift in Sources */, 54D9E0B327E4F51E003B9AD9 /* HotKeysController.swift in Sources */, C4B97B79275CF1B5003F3378 /* App+ActivationPolicy.swift in Sources */, C4CE3BBB27B324230086CA49 /* MainMenu+Switcher.swift in Sources */, diff --git a/phpmon-tests/Parsers/NginxConfigurationTest.swift b/phpmon-tests/Parsers/NginxConfigurationTest.swift index 15a73af..ddbc7fc 100644 --- a/phpmon-tests/Parsers/NginxConfigurationTest.swift +++ b/phpmon-tests/Parsers/NginxConfigurationTest.swift @@ -37,43 +37,43 @@ class NginxConfigurationTest: XCTestCase { func testCanDetermineSiteNameAndTld() throws { XCTAssertEqual( "nginx-site", - NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain + NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.domain ) XCTAssertEqual( "test", - NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld + NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.tld ) } func testCanDetermineIsolation() throws { XCTAssertNil( - NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion + NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)?.isolatedVersion ) XCTAssertEqual( "8.1", - NginxConfiguration.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion + NginxConfigurationFile.from(filePath: NginxConfigurationTest.isolatedUrl.path)?.isolatedVersion ) } func testCanDetermineProxy() throws { - let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.proxyUrl.path)! + 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) - let normal = NginxConfiguration.from(filePath: NginxConfigurationTest.regularUrl.path)! + let normal = NginxConfigurationFile.from(filePath: NginxConfigurationTest.regularUrl.path)! XCTAssertFalse(normal.contents.contains("# valet stub: proxy.valet.conf")) XCTAssertEqual(nil, normal.proxy) } func testCanDetermineSecuredProxy() throws { - let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.secureProxyUrl.path)! + 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 testCanDetermineProxyWithCustomTld() throws { - let proxied = NginxConfiguration.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)! + let proxied = NginxConfigurationFile.from(filePath: NginxConfigurationTest.customTldProxyUrl.path)! XCTAssertTrue(proxied.contents.contains("# valet stub: secure.proxy.valet.conf")) XCTAssertEqual("http://localhost:8080", proxied.proxy) } diff --git a/phpmon-tests/Parsers/PhpIniTest.swift b/phpmon-tests/Parsers/PhpConfigurationTest.swift similarity index 75% rename from phpmon-tests/Parsers/PhpIniTest.swift rename to phpmon-tests/Parsers/PhpConfigurationTest.swift index d6f20b6..c939778 100644 --- a/phpmon-tests/Parsers/PhpIniTest.swift +++ b/phpmon-tests/Parsers/PhpConfigurationTest.swift @@ -8,14 +8,14 @@ import XCTest -class PhpIniTest: XCTestCase { +class PhpConfigurationTest: XCTestCase { static var phpIniFileUrl: URL { return Bundle(for: Self.self).url(forResource: "php", withExtension: "ini")! } func testCanLoadExtension() throws { - let iniFile = PhpInitializationFile(fileUrl: Self.phpIniFileUrl) + let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path) XCTAssertNotNil(iniFile) } diff --git a/phpmon/Common/PHP/ActivePhpInstallation.swift b/phpmon/Common/PHP/ActivePhpInstallation.swift index d3a4d68..cb1a0e2 100644 --- a/phpmon/Common/PHP/ActivePhpInstallation.swift +++ b/phpmon/Common/PHP/ActivePhpInstallation.swift @@ -52,11 +52,9 @@ class ActivePhpInstallation { // Load extension information let mainConfigurationFileUrl = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") - iniFiles.append( - PhpConfigurationFile(fileUrl: mainConfigurationFileUrl) - ) - - // extensions.append(contentsOf: PhpExtension.load(from: mainConfigurationFileUrl)) + if let file = PhpConfigurationFile.from(filePath: mainConfigurationFileUrl.path) { + iniFiles.append(file) + } // Get configuration values limits = Limits( @@ -73,11 +71,9 @@ class ActivePhpInstallation { // See if any extensions are present in said .ini files paths.forEach { (iniFilePath) in - let fileUrl = URL(fileURLWithPath: iniFilePath) - - iniFiles.append( - PhpConfigurationFile(fileUrl: fileUrl) - ) + if let file = PhpConfigurationFile.from(filePath: iniFilePath) { + iniFiles.append(file) + } } } diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift index c0ab7bb..cd59c42 100644 --- a/phpmon/Common/PHP/PhpConfigurationFile.swift +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -8,7 +8,7 @@ import Foundation -class PhpConfigurationFile { +class PhpConfigurationFile: CreatedFromFile { typealias Section = [String: String] typealias Config = [String: Section] @@ -22,14 +22,31 @@ class PhpConfigurationFile { /// The actual content of the configuration file. var content: Config - init(fileUrl: URL) { - self.file = fileUrl.path + static func from(filePath: String) -> Self? { + let path = filePath.replacingOccurrences( + of: "~", + with: "/Users/\(Paths.whoami)" + ) - let rawString = (try? String(contentsOf: fileUrl, encoding: .utf8)) ?? "" + do { + let fileContents = try String(contentsOfFile: path) - self.extensions = PhpExtension.load(from: fileUrl) + return Self.init( + path: path, + contents: fileContents + ) + } catch { + Log.warn("Could not read the PHP configuration file at: `\(filePath)`") + return nil + } + } - self.content = Self.parseConfig(from: rawString.components(separatedBy: "\n")) + required init(path: String, contents: String) { + self.file = path + + self.extensions = PhpExtension.load(from: URL(string: path)!) + + self.content = Self.parseConfig(from: contents.components(separatedBy: "\n")) dump(self) } diff --git a/phpmon/Common/Protocols/CreatedFromFile.swift b/phpmon/Common/Protocols/CreatedFromFile.swift new file mode 100644 index 0000000..0fb46b5 --- /dev/null +++ b/phpmon/Common/Protocols/CreatedFromFile.swift @@ -0,0 +1,15 @@ +// +// CreatedFromFile.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 15/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +protocol CreatedFromFile { + + static func from(filePath: String) -> Self? + +} diff --git a/phpmon/Domain/Integrations/Nginx/NginxConfiguration.swift b/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift similarity index 91% rename from phpmon/Domain/Integrations/Nginx/NginxConfiguration.swift rename to phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift index e27270b..85846c3 100644 --- a/phpmon/Domain/Integrations/Nginx/NginxConfiguration.swift +++ b/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift @@ -1,5 +1,5 @@ // -// NginxConfiguration.swift +// NginxConfigurationFile.swift // PHP Monitor // // Created by Nico Verbruggen on 15/03/2022. @@ -8,7 +8,7 @@ import Foundation -class NginxConfiguration { +class NginxConfigurationFile: CreatedFromFile { /** Contents of the Nginx file in question, as a string. */ var contents: String! @@ -19,7 +19,7 @@ class NginxConfiguration { /** The TLD of the domain, usually derived from the name of the file. */ var tld: String - static func from(filePath: String) -> NginxConfiguration? { + static func from(filePath: String) -> Self? { let path = filePath.replacingOccurrences( of: "~", with: "/Users/\(Paths.whoami)" @@ -27,7 +27,8 @@ class NginxConfiguration { do { let fileContents = try String(contentsOfFile: path) - return NginxConfiguration.init( + + return Self.init( path: path, contents: fileContents ) @@ -37,7 +38,7 @@ class NginxConfiguration { } } - init(path: String, contents: String) { + required init(path: String, contents: String) { let domain = String(path.split(separator: "/").last!) let tld = String(domain.split(separator: ".").last!) diff --git a/phpmon/Domain/Integrations/Valet/Proxies/ProxyScanner/ValetProxyScanner.swift b/phpmon/Domain/Integrations/Valet/Proxies/ProxyScanner/ValetProxyScanner.swift index 1941822..4c1f71e 100644 --- a/phpmon/Domain/Integrations/Valet/Proxies/ProxyScanner/ValetProxyScanner.swift +++ b/phpmon/Domain/Integrations/Valet/Proxies/ProxyScanner/ValetProxyScanner.swift @@ -14,7 +14,7 @@ class ValetProxyScanner: ProxyScanner { .default .contentsOfDirectory(atPath: directoryPath) .compactMap { - return NginxConfiguration.from(filePath: "\(directoryPath)/\($0)") + return NginxConfigurationFile.from(filePath: "\(directoryPath)/\($0)") } .filter { return $0.proxy != nil diff --git a/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift b/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift index f74e3bd..d1fd5a5 100644 --- a/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift +++ b/phpmon/Domain/Integrations/Valet/Proxies/ValetProxy.swift @@ -14,7 +14,7 @@ class ValetProxy: DomainListable { var target: String var secured: Bool = false - init(_ configuration: NginxConfiguration) { + init(_ configuration: NginxConfigurationFile) { self.domain = configuration.domain self.tld = configuration.tld self.target = configuration.proxy! diff --git a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift index 951ae04..398e2e2 100644 --- a/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift +++ b/phpmon/Domain/Integrations/Valet/Sites/ValetSite.swift @@ -225,7 +225,7 @@ class ValetSite: DomainListable { public static func isolatedVersion(_ filePath: String) -> String? { if Filesystem.fileExists(filePath) { - return NginxConfiguration + return NginxConfigurationFile .from(filePath: filePath)? .isolatedVersion ?? nil } From f9df86851cdd4b4612b83b33259723df739b9190 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 15 May 2022 15:42:01 +0200 Subject: [PATCH 06/58] =?UTF-8?q?=E2=9C=A8=20Tweak=20description=20about?= =?UTF-8?q?=20sudoers=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/App/Startup.swift | 6 ++++-- phpmon/Localizable.strings | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index 5016524..3c73208 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -146,13 +146,15 @@ class Startup { 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 + subtitleText: "startup.errors.sudoers_brew.subtitle".localized, + descriptionText: "startup.errors.sudoers_brew.desc".localized ), EnvironmentCheck( 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 + subtitleText: "startup.errors.sudoers_valet.subtitle".localized, + descriptionText: "startup.errors.sudoers_valet.desc".localized ), // ================================================================================= // Verify if the Homebrew services are running (as root). diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index cd918c6..17f0de2 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -299,7 +299,7 @@ problem manually, using your own Terminal app (this just shows you the output)." "alert.php_fpm_broken.description" = "If it's been a while, you can usually fix this by running `valet install`, which updates your PHP-FPM configuration.\n\nIf you are seeing this message and you are trying to run a pre-release version of PHP, it is possible that Valet does not support this pre-release version of PHP yet.\n\nIf that is the case, you can try the following workaround: edit the file at `~/.composer/vendor/laravel/valet/cli/Valet/Brew.php` and add e.g. `php@8.2` to the `SUPPORTED_PHP_VERSIONS` array. After editing the file, try running `valet install`. (This will, if all goes well, set up all the required Valet configuration files.)"; // PHP Monitor Cannot Start -"alert.cannot_start.title" = "PHP Monitor cannot start due to a configuration problem"; +"alert.cannot_start.title" = "PHP Monitor cannot start due to a problem with your system configuration"; "alert.cannot_start.subtitle" = "The issue you were just notified about is keeping PHP Monitor from functioning correctly."; "alert.cannot_start.description" = "You might not need to quit PHP Monitor and restart it. If you have fixed the issue (or don't remember what the exact issue is) you can click on Retry, which will have PHP Monitor retry the startup checks."; "alert.cannot_start.close" = "Quit"; @@ -353,10 +353,12 @@ You can do this by running `composer global update` in your terminal. After that /// Brew & sudoers "startup.errors.sudoers_brew.title" = "Brew has not been added to sudoers.d"; "startup.errors.sudoers_brew.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue."; +"startup.errors.sudoers_brew.desc" = "If you keep seeing this error, it is possible that there is a permission issue where PHP Monitor cannot validate the file, which can usually be resolved by running: `sudo chmod +r /private/etc/sudoers.d/brew`"; /// Valet & sudoers "startup.errors.sudoers_valet.title" = "Valet has not been added to sudoers.d"; "startup.errors.sudoers_valet.subtitle" = "You must run `sudo valet trust` to ensure Valet can start and stop services without having to use sudo every time. The app will not work correctly until you resolve this issue. If you did this before, please run `sudo valet trust` again."; +"startup.errors.sudoers_valet.desc" = "If you keep seeing this error, it is possible that there is a permission issue where PHP Monitor cannot validate the file, which can usually be resolved by running: `sudo chmod +r /private/etc/sudoers.d/valet`"; /// Cannot retrieve services "startup.errors.services_json_error.title" = "Cannot determine services status"; From 0579ebb1c1d890f96c2bcc7d52ba0521cb24c1dd Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 18 May 2022 19:23:56 +0200 Subject: [PATCH 07/58] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20More=20efficient=20e?= =?UTF-8?q?xtension=20parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Parsers/PhpConfigurationTest.swift | 12 ++++++++- phpmon-tests/Parsers/PhpExtensionTest.swift | 10 +++---- phpmon/Common/PHP/PhpConfigurationFile.swift | 22 +++++++++++++--- phpmon/Common/PHP/PhpExtension.swift | 26 ++++++++++--------- .../Nginx/NginxConfigurationFile.swift | 15 +++++------ 5 files changed, 54 insertions(+), 31 deletions(-) diff --git a/phpmon-tests/Parsers/PhpConfigurationTest.swift b/phpmon-tests/Parsers/PhpConfigurationTest.swift index c939778..de26703 100644 --- a/phpmon-tests/Parsers/PhpConfigurationTest.swift +++ b/phpmon-tests/Parsers/PhpConfigurationTest.swift @@ -15,9 +15,19 @@ class PhpConfigurationTest: XCTestCase { } func testCanLoadExtension() throws { - let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path) + let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! XCTAssertNotNil(iniFile) + + XCTAssertGreaterThan(iniFile.extensions.count, 0) + } + + func testCanSwapConfigurationValue() throws { + let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! + + let configurationFile = PhpConfigurationFile.from(filePath: destination.path) + + XCTAssertNotNil(configurationFile) } } diff --git a/phpmon-tests/Parsers/PhpExtensionTest.swift b/phpmon-tests/Parsers/PhpExtensionTest.swift index 375760b..856cfbf 100644 --- a/phpmon-tests/Parsers/PhpExtensionTest.swift +++ b/phpmon-tests/Parsers/PhpExtensionTest.swift @@ -15,13 +15,13 @@ class PhpExtensionTest: XCTestCase { } func testCanLoadExtension() throws { - let extensions = PhpExtension.load(from: Self.phpIniFileUrl) + let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) XCTAssertGreaterThan(extensions.count, 0) } func testExtensionNameIsCorrect() throws { - let extensions = PhpExtension.load(from: Self.phpIniFileUrl) + let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) let extensionNames = extensions.map { (ext) -> String in return ext.name @@ -40,7 +40,7 @@ class PhpExtensionTest: XCTestCase { } func testExtensionStatusIsCorrect() throws { - let extensions = PhpExtension.load(from: Self.phpIniFileUrl) + let extensions = PhpExtension.from(filePath: Self.phpIniFileUrl.path) // xdebug should be enabled XCTAssertEqual(extensions[0].enabled, true) @@ -51,7 +51,7 @@ class PhpExtensionTest: XCTestCase { func testToggleWorksAsExpected() throws { let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! - let extensions = PhpExtension.load(from: destination) + let extensions = PhpExtension.from(filePath: destination.path) XCTAssertEqual(extensions.count, 6) // Try to disable xdebug (should be detected first)! @@ -66,7 +66,7 @@ class PhpExtensionTest: XCTestCase { XCTAssertTrue(file.contains("; zend_extension=\"xdebug.so\"")) // Make sure if we load the data again, it's disabled - XCTAssertEqual(PhpExtension.load(from: destination).first!.enabled, false) + XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false) } func testCanRetrieveXdebugMode() throws { diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift index cd59c42..8642341 100644 --- a/phpmon/Common/PHP/PhpConfigurationFile.swift +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -22,6 +22,7 @@ class PhpConfigurationFile: CreatedFromFile { /// The actual content of the configuration file. var content: Config + /** Resolves a PHP configuration file (.ini) */ static func from(filePath: String) -> Self? { let path = filePath.replacingOccurrences( of: "~", @@ -44,11 +45,24 @@ class PhpConfigurationFile: CreatedFromFile { required init(path: String, contents: String) { self.file = path - self.extensions = PhpExtension.load(from: URL(string: path)!) + let lines = contents.components(separatedBy: "\n") - self.content = Self.parseConfig(from: contents.components(separatedBy: "\n")) + self.extensions = PhpExtension.from(lines, filePath: path) + self.content = Self.parseConfig(lines: lines) + } - dump(self) + // MARK: API + + public func has(key: String) { + // TODO + } + + public func value(for key: String) { + // TODO + } + + public func replace(key: String, value: String) { + // TODO } // MARK: Parsing Logic @@ -58,7 +72,7 @@ class PhpConfigurationFile: CreatedFromFile { Attempts to parse the configuration file, based on an array of strings. Each string is a line from the configuration file. */ - private static func parseConfig(from lines: [String]) -> Config { + private static func parseConfig(lines: [String]) -> Config { var config = Config() var currentSectionName = "main" diff --git a/phpmon/Common/PHP/PhpExtension.swift b/phpmon/Common/PHP/PhpExtension.swift index c6d86d7..84a63a9 100644 --- a/phpmon/Common/PHP/PhpExtension.swift +++ b/phpmon/Common/PHP/PhpExtension.swift @@ -89,24 +89,26 @@ class PhpExtension { // MARK: - Static Methods - /** - This method will attempt to identify all extensions in the .ini file at a certain URL. - */ - static func load(from path: URL) -> [PhpExtension] { - let file = try? String(contentsOf: path, encoding: .utf8) + static func from(_ lines: [String], filePath: String) -> [PhpExtension] { + return lines.filter { + return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil + }.map { + return PhpExtension($0, file: filePath) + } + } + + static func from(filePath: String) -> [PhpExtension] { + let file = try? String(contentsOfFile: filePath) if file == nil { Log.err("There was an issue reading the file. Assuming no extensions were found.") return [] } - return file!.components(separatedBy: "\n") - .filter { - return $0.range(of: Self.extensionRegex, options: .regularExpression) != nil - } - .map { - return PhpExtension($0, file: path.path) - } + return Self.from( + file!.components(separatedBy: "\n"), + filePath: filePath + ) } } diff --git a/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift b/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift index 85846c3..c8dade6 100644 --- a/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift +++ b/phpmon/Domain/Integrations/Nginx/NginxConfigurationFile.swift @@ -10,15 +10,16 @@ import Foundation class NginxConfigurationFile: CreatedFromFile { - /** Contents of the Nginx file in question, as a string. */ + /// Contents of the Nginx file in question, as a string. var contents: String! - /** The name of the domain, usually derived from the name of the file. */ + /// The name of the domain, usually derived from the name of the file. var domain: String - /** The TLD of the domain, usually derived from the name of the file. */ + /// The TLD of the domain, usually derived from the name of the file. var tld: String + /** Resolves an nginx configuration file (.conf) */ static func from(filePath: String) -> Self? { let path = filePath.replacingOccurrences( of: "~", @@ -47,9 +48,7 @@ class NginxConfigurationFile: CreatedFromFile { self.tld = tld } - /** - Retrieves what address this domain is proxying. - */ + /** Retrieves what address this domain is proxying. */ lazy var proxy: String? = { let regex = try! NSRegularExpression( pattern: #"proxy_pass (?.*:\d*)(\/*);"#, @@ -62,9 +61,7 @@ class NginxConfigurationFile: CreatedFromFile { return contents[Range(match.range(withName: "proxy"), in: contents)!] }() - /** - Retrieves which isolated version is active for this domain (if applicable). - */ + /** Retrieves which isolated version is active for this domain (if applicable). */ lazy var isolatedVersion: String? = { let regex = try! NSRegularExpression( // PHP versions have (so far) never needed multiple digits for version numbers From 2e61479c75eb0e9df0cc1492629145c83f35e922 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 18 May 2022 19:45:16 +0200 Subject: [PATCH 08/58] =?UTF-8?q?=E2=9C=A8=20Allow=20reading=20of=20config?= =?UTF-8?q?uration=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Parsers/PhpConfigurationTest.swift | 18 ++++++++++++++++++ phpmon/Common/PHP/PhpConfigurationFile.swift | 15 +++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/phpmon-tests/Parsers/PhpConfigurationTest.swift b/phpmon-tests/Parsers/PhpConfigurationTest.swift index de26703..750037b 100644 --- a/phpmon-tests/Parsers/PhpConfigurationTest.swift +++ b/phpmon-tests/Parsers/PhpConfigurationTest.swift @@ -22,6 +22,24 @@ class PhpConfigurationTest: XCTestCase { XCTAssertGreaterThan(iniFile.extensions.count, 0) } + func testCanCheckKeyExistence() throws { + let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! + + XCTAssertTrue(iniFile.has(key: "error_reporting")) + XCTAssertTrue(iniFile.has(key: "display_errors")) + XCTAssertFalse(iniFile.has(key: "my_unknown_key")) + } + + func testCanCheckKeyValue() throws { + let iniFile = PhpConfigurationFile.from(filePath: Self.phpIniFileUrl.path)! + + XCTAssertNotNil(iniFile.get(for: "error_reporting")) + XCTAssert(iniFile.get(for: "error_reporting") == "E_ALL") + + XCTAssertNotNil(iniFile.get(for: "display_errors")) + XCTAssert(iniFile.get(for: "display_errors") == "On") + } + func testCanSwapConfigurationValue() throws { let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift index 8642341..df7ec35 100644 --- a/phpmon/Common/PHP/PhpConfigurationFile.swift +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -53,12 +53,19 @@ class PhpConfigurationFile: CreatedFromFile { // MARK: API - public func has(key: String) { - // TODO + public func has(key: String) -> Bool { + return self.content.contains { (_: String, section: Section) in + return section.keys.contains(key) + } } - public func value(for key: String) { - // TODO + public func get(for key: String) -> String? { + for (_, section) in self.content { + if section.keys.contains(key) { + return section[key] + } + } + return nil } public func replace(key: String, value: String) { From 990152d77df5d5a0c0c5722cb7ced8ae8d0b2f52 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 00:08:26 +0200 Subject: [PATCH 09/58] =?UTF-8?q?=E2=9C=A8=20Allow=20replacing=20of=20conf?= =?UTF-8?q?ig=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Parsers/PhpConfigurationTest.swift | 43 ++++++++-- phpmon/Common/PHP/PhpConfigurationFile.swift | 78 +++++++++++++++---- 2 files changed, 102 insertions(+), 19 deletions(-) diff --git a/phpmon-tests/Parsers/PhpConfigurationTest.swift b/phpmon-tests/Parsers/PhpConfigurationTest.swift index 750037b..3ec73d4 100644 --- a/phpmon-tests/Parsers/PhpConfigurationTest.swift +++ b/phpmon-tests/Parsers/PhpConfigurationTest.swift @@ -1,5 +1,5 @@ // -// PhpIniTest.swift +// PhpConfigurationTest.swift // PHP Monitor // // Created by Nico Verbruggen on 04/05/2022. @@ -40,12 +40,45 @@ class PhpConfigurationTest: XCTestCase { XCTAssert(iniFile.get(for: "display_errors") == "On") } - func testCanSwapConfigurationValue() throws { - let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! + func testCanCustomizeConfigurationValue() throws { + let destination = Utility + .copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! - let configurationFile = PhpConfigurationFile.from(filePath: destination.path) + let configurationFile = PhpConfigurationFile + .from(filePath: destination.path)! - XCTAssertNotNil(configurationFile) + // 0. Verify the original value + XCTAssertEqual(configurationFile.get(for: "error_reporting"), "E_ALL") + + // 1. Change the value + try! configurationFile.replace( + key: "error_reporting", + value: "E_ALL & ~E_DEPRECATED & ~E_STRICT" + ) + XCTAssertEqual( + configurationFile.get(for: "error_reporting"), + "E_ALL & ~E_DEPRECATED & ~E_STRICT" + ) + + // 2. Ensure that same key and value doesn't break subsequent saves + try! configurationFile.replace( + key: "error_reporting", + value: "error_reporting" + ) + XCTAssertEqual( + configurationFile.get(for: "error_reporting"), + "error_reporting" + ) + + // 3. Verify subsequent saves weren't broken + try! configurationFile.replace( + key: "error_reporting", + value: "E_ALL" + ) + XCTAssertEqual( + configurationFile.get(for: "error_reporting"), + "E_ALL" + ) } } diff --git a/phpmon/Common/PHP/PhpConfigurationFile.swift b/phpmon/Common/PHP/PhpConfigurationFile.swift index df7ec35..bd01dd3 100644 --- a/phpmon/Common/PHP/PhpConfigurationFile.swift +++ b/phpmon/Common/PHP/PhpConfigurationFile.swift @@ -1,5 +1,5 @@ // -// PhpInitFile.swift +// PhpConfigurationFile.swift // PHP Monitor // // Created by Nico Verbruggen on 04/05/2022. @@ -10,18 +10,26 @@ import Foundation class PhpConfigurationFile: CreatedFromFile { - typealias Section = [String: String] + struct ConfigValue { + let lineIndex: Int + let value: String + } + + typealias Section = [String: ConfigValue] typealias Config = [String: Section] /// The file where this configuration file was located. - let file: String + let filePath: String /// The extensions found in this .ini file. - let extensions: [PhpExtension] + var extensions: [PhpExtension] - /// The actual content of the configuration file. + /// The actual, structured content of the configuration file. var content: Config + /// The original lines of the file. + var lines: [String] + /** Resolves a PHP configuration file (.ini) */ static func from(filePath: String) -> Self? { let path = filePath.replacingOccurrences( @@ -43,10 +51,8 @@ class PhpConfigurationFile: CreatedFromFile { } required init(path: String, contents: String) { - self.file = path - - let lines = contents.components(separatedBy: "\n") - + self.filePath = path + self.lines = contents.components(separatedBy: "\n") self.extensions = PhpExtension.from(lines, filePath: path) self.content = Self.parseConfig(lines: lines) } @@ -60,16 +66,57 @@ class PhpConfigurationFile: CreatedFromFile { } public func get(for key: String) -> String? { + return getConfig(for: key)?.value + } + + public func getConfig(for key: String) -> ConfigValue? { for (_, section) in self.content { if section.keys.contains(key) { - return section[key] + return section[key]! } } return nil } - public func replace(key: String, value: String) { - // TODO + enum ReplacementErrors: Error { + case missingKey + } + + /** + Replaces the value for a specific (existing) key with a new value. + The key must exist for this to work. + */ + public func replace(key: String, value: String) throws { + // Ensure that the key exists + guard let item = getConfig(for: key) else { + throw ReplacementErrors.missingKey + } + + // Figure out what comes after the assignment + var components = self + .lines[item.lineIndex] + .components(separatedBy: "=") + + // Replace the value with the new one + components[1] = components[1] + .replacingOccurrences(of: item.value, with: value) + + // Replace the specific line + self.lines[item.lineIndex] = components.joined(separator: "=") + + // Finally, join the string and save the file atomatically again + try self.lines.joined(separator: "\n") + .write(toFile: self.filePath, atomically: true, encoding: .utf8) + + // Reload the original file + self.reload() + } + + public func reload() { + self.lines = try! String(contentsOfFile: self.filePath) + .components(separatedBy: "\n") + self.extensions = PhpExtension.from(lines, filePath: self.filePath) + self.content = Self.parseConfig(lines: lines) } // MARK: Parsing Logic @@ -84,14 +131,17 @@ class PhpConfigurationFile: CreatedFromFile { var currentSectionName = "main" - for line in lines { + for (index, line) in lines.enumerated() { let line = trim(line) if line.hasPrefix("[") && line.hasSuffix("]") { currentSectionName = parseSectionHeader(line) } else if let (key, value) = parseLine(line) { var section = config[currentSectionName] ?? [:] - section[key] = value + section[key] = ConfigValue( + lineIndex: index, + value: value + ) config[currentSectionName] = section } } From e7f80ebce8eaf3df9b8dd89825b80cffe06e26e5 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 01:05:06 +0200 Subject: [PATCH 10/58] =?UTF-8?q?=E2=9C=A8=20Switch=20Xdebug=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Common/PHP/Extensions/Xdebug.swift | 6 +++++- phpmon/Common/PHP/PHP Version/PhpEnv.swift | 10 ++++++++++ phpmon/Domain/Menu/MainMenu.swift | 17 +++++++++++++++++ phpmon/Domain/Menu/StatusMenu.swift | 2 +- phpmon/Domain/Watcher/App+ConfigWatch.swift | 8 ++++---- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/phpmon/Common/PHP/Extensions/Xdebug.swift b/phpmon/Common/PHP/Extensions/Xdebug.swift index e120110..e9bb59d 100644 --- a/phpmon/Common/PHP/Extensions/Xdebug.swift +++ b/phpmon/Common/PHP/Extensions/Xdebug.swift @@ -15,7 +15,11 @@ class Xdebug { } public static var mode: String { - return Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"]) + guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + return "" + } + + return file.get(for: "xdebug.mode") ?? "" } public static var modes: [String] { diff --git a/phpmon/Common/PHP/PHP Version/PhpEnv.swift b/phpmon/Common/PHP/PHP Version/PhpEnv.swift index 54c36f9..4dc3e88 100644 --- a/phpmon/Common/PHP/PHP Version/PhpEnv.swift +++ b/phpmon/Common/PHP/PHP Version/PhpEnv.swift @@ -174,4 +174,14 @@ class PhpEnv { return false } + + /** + Returns the configuration file instance that is used for a specific config value. + You can then use the configuration file instance to change values. + */ + public func getConfigFile(forKey key: String) -> PhpConfigurationFile? { + return PhpEnv.phpInstall.iniFiles + .reversed() + .first(where: { $0.has(key: key) }) + } } diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 69fb17b..b8572a1 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -244,6 +244,23 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate @objc func toggleXdebugMode(sender: XdebugMenuItem) { Log.info("Switching Xdebug to mode: \(sender.mode)") + + guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + Log.info("xdebug.mode could not be found in any .ini file, aborting.") + return + } + + do { + // Replace the xdebug mode + try file.replace(key: "xdebug.mode", value: sender.mode) + // Refresh the menu + Log.perf("Refreshing menu...") + MainMenu.shared.rebuild() + // Restart PHP-FPM + restartPhpFpm() + } catch { + Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") + } } @objc func toggleExtension(sender: ExtensionMenuItem) { diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index 1647d51..c2c6e0f 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -68,7 +68,7 @@ class StatusMenu: NSMenu { self.addItem(NSMenuItem.separator()) - // self.addXdebugMenuItem() + self.addXdebugMenuItem() self.addFirstAidAndServicesMenuItems() } diff --git a/phpmon/Domain/Watcher/App+ConfigWatch.swift b/phpmon/Domain/Watcher/App+ConfigWatch.swift index 6c57a9b..bde582f 100644 --- a/phpmon/Domain/Watcher/App+ConfigWatch.swift +++ b/phpmon/Domain/Watcher/App+ConfigWatch.swift @@ -11,16 +11,16 @@ import Foundation extension App { func startWatcher(_ url: URL) { - Log.info("No watcher currently active...") + Log.perf("No watcher currently active...") self.watcher = PhpConfigWatcher(for: url) self.watcher.didChange = { url in - Log.info("Something has changed in: \(url)") + Log.perf("Something has changed in: \(url)") // Check if the watcher has last updated the menu less than 0.75s ago let distance = self.watcher.lastUpdate?.distance(to: Date().timeIntervalSince1970) if distance == nil || distance != nil && distance! > 0.75 { - Log.info("Refreshing menu...") + Log.perf("Refreshing menu...") MainMenu.shared.reloadPhpMonitorMenuInBackground() self.watcher.lastUpdate = Date().timeIntervalSince1970 } @@ -43,7 +43,7 @@ extension App { if self.watcher.url != url || forceReload { self.watcher.disable() self.watcher = nil - Log.info("Watcher has stopped watching files. Starting new one...") + Log.perf("Watcher has stopped watching files. Starting new one...") self.startWatcher(url) } } From 40e404fe247543b0dcca369abc0b5f9f51c3bcf8 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 01:50:15 +0200 Subject: [PATCH 11/58] =?UTF-8?q?=F0=9F=91=8C=20Prototype=20(non-functiona?= =?UTF-8?q?l)=20for=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 12 +++++++ .../Test Files/phpmon/phpmon-config.json | 24 ++++++++++++++ phpmon/Domain/Menu/StatusMenu.swift | 31 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 phpmon-tests/Test Files/phpmon/phpmon-config.json diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 8c19794..4fe01ae 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -117,6 +117,7 @@ C44CCD4127AFE2FC00CE40E5 /* AlertableError.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */; }; C44CCD4927AFF3B700CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; + C44F868E2835BD8D005C353A /* phpmon-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C44F868D2835BD8D005C353A /* phpmon-config.json */; }; C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */; }; C464ADAC275A7A3F003FCD53 /* DomainListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */; }; C464ADAD275A7A3F003FCD53 /* DomainListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */; }; @@ -349,6 +350,7 @@ C44C1990276E44CB0072762D /* ProgressWindow.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ProgressWindow.storyboard; sourceTree = ""; }; C44CCD3F27AFE2FC00CE40E5 /* AlertableError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertableError.swift; sourceTree = ""; }; C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Async.swift"; sourceTree = ""; }; + C44F868D2835BD8D005C353A /* phpmon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "phpmon-config.json"; sourceTree = ""; }; C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-proxy.test"; sourceTree = ""; }; C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListWC.swift; sourceTree = ""; }; C464ADAE275A7A69003FCD53 /* DomainListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListVC.swift; sourceTree = ""; }; @@ -549,6 +551,7 @@ C40C7F1C27720E1400DDDCDC /* Test Files */ = { isa = PBXGroup; children = ( + C44F868C2835BD60005C353A /* phpmon */, C459B4C127F6097E00E9B4B4 /* php */, C459B4C027F6096300E9B4B4 /* valet */, C459B4BF27F6094100E9B4B4 /* brew */, @@ -678,6 +681,14 @@ path = Errors; sourceTree = ""; }; + C44F868C2835BD60005C353A /* phpmon */ = { + isa = PBXGroup; + children = ( + C44F868D2835BD8D005C353A /* phpmon-config.json */, + ); + path = phpmon; + sourceTree = ""; + }; C459B4BE27F6093A00E9B4B4 /* nginx */ = { isa = PBXGroup; children = ( @@ -1117,6 +1128,7 @@ C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */, C4F780A825D80AE8000DBC97 /* php.ini in Resources */, C4068CA527B0780A00544CD5 /* CheckboxPreferenceView.xib in Resources */, + C44F868E2835BD8D005C353A /* phpmon-config.json in Resources */, C43A8A2025D9D1D700591B77 /* brew-formula.json in Resources */, C4AF9F72275445FF00D44ED0 /* valet-config.json in Resources */, C44C1992276E44CB0072762D /* ProgressWindow.storyboard in Resources */, diff --git a/phpmon-tests/Test Files/phpmon/phpmon-config.json b/phpmon-tests/Test Files/phpmon/phpmon-config.json new file mode 100644 index 0000000..1ea101f --- /dev/null +++ b/phpmon-tests/Test Files/phpmon/phpmon-config.json @@ -0,0 +1,24 @@ +{ + "scan_apps": [], + "presets": [ + { + "name": "Default PHP", + "extensions": { + "xdebug": false + }, + "ini": { + "memory_limit": "128M" + } + }, + { + "name": "Personal Website", + "extensions": { + "xdebug": true + }, + "ini": { + "xdebug.mode": "coverage", + "memory_limit": "512M" + } + } + ] +} \ No newline at end of file diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index c2c6e0f..939eac9 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -69,6 +69,7 @@ class StatusMenu: NSMenu { self.addItem(NSMenuItem.separator()) self.addXdebugMenuItem() + self.addPresetsMenuItem() self.addFirstAidAndServicesMenuItems() } @@ -140,6 +141,36 @@ class StatusMenu: NSMenu { } } + func addPresetsMenuItem() { + let presets = NSMenuItem(title: "Configuration Presets", action: nil, keyEquivalent: "") + let presetsMenu = NSMenu() + presetsMenu.addItem(NSMenuItem.separator()) + presetsMenu.addItem(HeaderView.asMenuItem(text: "Apply Configuration Presets")) + presetsMenu.addItem(NSMenuItem( + title: "Default Configuration (1 extension, 1 pref)", + action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "") + ) + presetsMenu.addItem(NSMenuItem( + title: "Personal Website (1 extension, 2 prefs)", + action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "") + ) + presetsMenu.addItem(NSMenuItem.separator()) + presetsMenu.addItem(NSMenuItem( + title: "Revert to Previous Configuration...", + action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "") + ) + presetsMenu.addItem(NSMenuItem.separator()) + presetsMenu.addItem(NSMenuItem( + title: "2 profiles loaded from configuration file", + action: nil, keyEquivalent: "") + ) + for item in presetsMenu.items { + item.target = MainMenu.shared + } + self.setSubmenu(presetsMenu, for: presets) + self.addItem(presets) + } + func addXdebugMenuItem() { if !Xdebug.enabled { return From db8197df3d9db93a8f08af0b5809b0a658cf67c2 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 19:06:03 +0200 Subject: [PATCH 12/58] =?UTF-8?q?=F0=9F=91=8C=20Handle=20multiple=20modes?= =?UTF-8?q?=20w/=20Xdebug=20menu=20item?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit also fixes the width of the header items. --- phpmon-tests/Parsers/PhpExtensionTest.swift | 5 ----- phpmon/Common/PHP/Extensions/Xdebug.swift | 13 ++++++++----- .../Integrations/Composer/PhpFrameworks.swift | 2 -- phpmon/Domain/Menu/HeaderView.swift | 17 ++++++++++++++--- phpmon/Domain/Menu/MainMenu.swift | 4 ++++ phpmon/Domain/Menu/StatusMenu.swift | 14 ++++++++++++-- phpmon/Localizable.strings | 2 +- 7 files changed, 39 insertions(+), 18 deletions(-) diff --git a/phpmon-tests/Parsers/PhpExtensionTest.swift b/phpmon-tests/Parsers/PhpExtensionTest.swift index 856cfbf..0e38c5e 100644 --- a/phpmon-tests/Parsers/PhpExtensionTest.swift +++ b/phpmon-tests/Parsers/PhpExtensionTest.swift @@ -69,9 +69,4 @@ class PhpExtensionTest: XCTestCase { XCTAssertEqual(PhpExtension.from(filePath: destination.path).first!.enabled, false) } - func testCanRetrieveXdebugMode() throws { - let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('xdebug.mode');"]) - XCTAssertEqual(value, "coverage") - } - } diff --git a/phpmon/Common/PHP/Extensions/Xdebug.swift b/phpmon/Common/PHP/Extensions/Xdebug.swift index e9bb59d..a340a6a 100644 --- a/phpmon/Common/PHP/Extensions/Xdebug.swift +++ b/phpmon/Common/PHP/Extensions/Xdebug.swift @@ -11,20 +11,23 @@ import Foundation class Xdebug { public static var enabled: Bool { - return !self.mode.isEmpty + return PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") != nil } - public static var mode: String { + public static var activeModes: [String] { guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { - return "" + return [] } - return file.get(for: "xdebug.mode") ?? "" + guard let value = file.get(for: "xdebug.mode") else { + return [] + } + + return value.components(separatedBy: ",").filter { self.modes.contains($0) } } public static var modes: [String] { return [ - "off", "develop", "coverage", "debug", diff --git a/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift b/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift index 158ebbc..fa72d92 100644 --- a/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift +++ b/phpmon/Domain/Integrations/Composer/PhpFrameworks.swift @@ -38,8 +38,6 @@ struct PhpFrameworks { "zendframework/zendframework": "Zend", "zendframework/zend-mvc": "Zend", "typo3/cms-core": "Typo3" - - // TODO (6.0): Handle these in v6.0 // "magento/*": "Magento", // "concrete5/*": "Concrete5", // "contao/*": "Contao", diff --git a/phpmon/Domain/Menu/HeaderView.swift b/phpmon/Domain/Menu/HeaderView.swift index eb245e1..0f7d23c 100644 --- a/phpmon/Domain/Menu/HeaderView.swift +++ b/phpmon/Domain/Menu/HeaderView.swift @@ -13,12 +13,23 @@ class HeaderView: NSView, XibLoadable { @IBOutlet weak var textField: NSTextField! - static func asMenuItem(text: String) -> NSMenuItem { - let view = Self.createFromXib() - view!.textField.stringValue = text.uppercased() + static func asMenuItem( + text: String, + width: Int? = nil + ) -> NSMenuItem { + let view = Self.createFromXib()! + + view.autoresizingMask = [.width, .height] + + view.textField.stringValue = text.uppercased() + view.textField.sizeToFit() + + view.setFrameSize(CGSize(width: view.textField.frame.width + 40, height: view.frame.height)) + let item = NSMenuItem() item.view = view item.target = self + return item } diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index b8572a1..31dcad5 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -242,6 +242,10 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate } } + @objc func disableAllXdebugModes() { + // TODO + } + @objc func toggleXdebugMode(sender: XdebugMenuItem) { Log.info("Switching Xdebug to mode: \(sender.mode)") diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index c2c6e0f..333b14f 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -151,7 +151,9 @@ class StatusMenu: NSMenu { keyEquivalent: "" ) let xdebugModesMenu = NSMenu() - let xdebugMode = Xdebug.mode + let activeModes = Xdebug.activeModes + + xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "Available Modes")) for mode in Xdebug.modes { let item = XdebugMenuItem( @@ -159,11 +161,19 @@ class StatusMenu: NSMenu { action: #selector(MainMenu.toggleXdebugMode(sender:)), keyEquivalent: "" ) - item.state = xdebugMode == mode ? .on : .off + + item.state = activeModes.contains(mode) ? .on : .off item.mode = mode xdebugModesMenu.addItem(item) } + xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "Actions")) + xdebugModesMenu.addItem( + withTitle: "Disable All", + action: #selector(MainMenu.disableAllXdebugModes), + keyEquivalent: "" + ) + for item in xdebugModesMenu.items { item.target = MainMenu.shared } diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 17f0de2..98b6421 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -43,7 +43,7 @@ "mi_other" = "First Aid & Services"; "mi_first_aid" = "First Aid"; -"mi_xdebug_mode" = "Switch Xdebug Mode"; +"mi_xdebug_mode" = "Manage Xdebug"; "mi_composer" = "Composer"; "mi_valet_config" = "Locate Valet Folder (.config/valet)"; From 83eac7bf046016d263f2270aaaa3877f987f225e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 20:01:28 +0200 Subject: [PATCH 13/58] =?UTF-8?q?=F0=9F=91=8C=20Save=20multiple=20Xdebug?= =?UTF-8?q?=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/Menu/MainMenu.swift | 34 +++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 31dcad5..2f7d00b 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -243,7 +243,20 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate } @objc func disableAllXdebugModes() { - // TODO + guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + Log.info("xdebug.mode could not be found in any .ini file, aborting.") + return + } + + do { + try file.replace(key: "xdebug.mode", value: "off") + + Log.perf("Refreshing menu...") + MainMenu.shared.rebuild() + restartPhpFpm() + } catch { + Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") + } } @objc func toggleXdebugMode(sender: XdebugMenuItem) { @@ -255,12 +268,25 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate } do { + // Get the active modes + var modes = Xdebug.activeModes + // Set the new modes + if let index = modes.firstIndex(of: sender.mode) { + modes.remove(at: index) + } else { + modes.append(sender.mode) + } + // Replace the xdebug mode - try file.replace(key: "xdebug.mode", value: sender.mode) - // Refresh the menu + var newValue = modes.joined(separator: ",") + if newValue.isEmpty { + newValue = "off" + } + + try file.replace(key: "xdebug.mode", value: newValue) + Log.perf("Refreshing menu...") MainMenu.shared.rebuild() - // Restart PHP-FPM restartPhpFpm() } catch { Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") From 7709cd9f6c2695f1c3f83a918c291e3d397d47cd Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 20:12:30 +0200 Subject: [PATCH 14/58] =?UTF-8?q?=F0=9F=91=8C=20Cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 8 +- phpmon/Domain/Menu/MainMenu+Actions.swift | 204 ++++++++++++++++++++++ phpmon/Domain/Menu/MainMenu.swift | 203 --------------------- 3 files changed, 210 insertions(+), 205 deletions(-) create mode 100644 phpmon/Domain/Menu/MainMenu+Actions.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 8c19794..a01e18a 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -242,6 +242,7 @@ C4F30B0A278E1A1A00755FCE /* ComposerJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D89BC52783C99400A02B68 /* ComposerJson.swift */; }; C4F30B0B278E203C00755FCE /* MainMenu+Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C3ED402783497000AB15D8 /* MainMenu+Startup.swift */; }; C4F319C927B034A500AFF46F /* Stats.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DEB7D327A5D60B00834718 /* Stats.swift */; }; + C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F361602836BFD9003598CC /* MainMenu+Actions.swift */; }; C4F5FBCD28218CB8001065C5 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; }; C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; }; C4F780A825D80AE8000DBC97 /* php.ini in Resources */ = {isa = PBXBuildFile; fileRef = C4F780A725D80AE8000DBC97 /* php.ini */; }; @@ -426,6 +427,7 @@ C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = ""; }; C4F30B02278E16BA00755FCE /* HomebrewService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewService.swift; sourceTree = ""; }; C4F30B06278E195800755FCE /* brew-services.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "brew-services.json"; sourceTree = ""; }; + C4F361602836BFD9003598CC /* MainMenu+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Actions.swift"; sourceTree = ""; }; C4F5FBCC28218C93001065C5 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; C4F7807925D7F84B000DBC97 /* phpmon-tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "phpmon-tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C4F7807D25D7F84B000DBC97 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -738,6 +740,7 @@ C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */, C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */, C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */, + C4F361602836BFD9003598CC /* MainMenu+Actions.swift */, C47331A1247093B7009A0597 /* StatusMenu.swift */, C48D0C9525CC80B100CC7490 /* HeaderView.swift */, C48D0C9925CC888B00CC7490 /* HeaderView.xib */, @@ -1197,6 +1200,7 @@ C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */, C415D3B72770F294005EF286 /* Actions.swift in Sources */, C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */, + C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */, C44C198D276E3A1C0072762D /* ProgressWindow.swift in Sources */, 54D9E0B827E4F51E003B9AD9 /* KeyCombo.swift in Sources */, C4C0E8E727F88B41002D32A9 /* ProxyScanner.swift in Sources */, @@ -1544,7 +1548,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 790; + CURRENT_PROJECT_VERSION = 795; DEBUG = YES; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; @@ -1570,7 +1574,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 790; + CURRENT_PROJECT_VERSION = 795; DEBUG = NO; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift new file mode 100644 index 0000000..e64cc7e --- /dev/null +++ b/phpmon/Domain/Menu/MainMenu+Actions.swift @@ -0,0 +1,204 @@ +// +// MainMenu+Actions.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 19/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +extension MainMenu { + + // MARK: - Actions + + @objc func fixHomebrewPermissions() { + if !BetterAlert() + .withInformation( + title: "alert.fix_homebrew_permissions.title".localized, + subtitle: "alert.fix_homebrew_permissions.subtitle".localized, + description: "alert.fix_homebrew_permissions.desc".localized + ) + .withPrimary(text: "alert.fix_homebrew_permissions.ok".localized) + .withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized) + .didSelectPrimary() { + return + } + + asyncExecution { + try Actions.fixHomebrewPermissions() + } success: { + BetterAlert() + .withInformation( + title: "alert.fix_homebrew_permissions_done.title".localized, + subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized, + description: "alert.fix_homebrew_permissions_done.desc".localized + ) + .withPrimary(text: "OK") + .show() + } failure: { error in + BetterAlert.show(for: error as! HomebrewPermissionError) + } + } + + @objc func restartPhpFpm() { + asyncExecution { + Actions.restartPhpFpm() + } + } + + @objc func restartAllServices() { + asyncExecution { + Actions.restartDnsMasq() + Actions.restartPhpFpm() + Actions.restartNginx() + } success: { + DispatchQueue.main.async { + LocalNotification.send( + title: "notification.services_restarted".localized, + subtitle: "notification.services_restarted_desc".localized + ) + } + } + } + + @objc func stopAllServices() { + asyncExecution { + Actions.stopAllServices() + } success: { + DispatchQueue.main.async { + LocalNotification.send( + title: "notification.services_stopped".localized, + subtitle: "notification.services_stopped_desc".localized + ) + } + } + } + + @objc func restartNginx() { + asyncExecution { + Actions.restartNginx() + } + } + + @objc func restartDnsMasq() { + asyncExecution { + Actions.restartDnsMasq() + } + } + + @objc func disableAllXdebugModes() { + guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + Log.info("xdebug.mode could not be found in any .ini file, aborting.") + return + } + + do { + try file.replace(key: "xdebug.mode", value: "off") + + Log.perf("Refreshing menu...") + MainMenu.shared.rebuild() + restartPhpFpm() + } catch { + Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") + } + } + + @objc func toggleXdebugMode(sender: XdebugMenuItem) { + Log.info("Switching Xdebug to mode: \(sender.mode)") + + guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { + return Log.info("xdebug.mode could not be found in any .ini file, aborting.") + } + + do { + var modes = Xdebug.activeModes + + if let index = modes.firstIndex(of: sender.mode) { + modes.remove(at: index) + } else { + modes.append(sender.mode) + } + + var newValue = modes.joined(separator: ",") + if newValue.isEmpty { + newValue = "off" + } + + try file.replace(key: "xdebug.mode", value: newValue) + + Log.perf("Refreshing menu...") + MainMenu.shared.rebuild() + restartPhpFpm() + } catch { + Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") + } + } + + @objc func toggleExtension(sender: ExtensionMenuItem) { + asyncExecution { + sender.phpExtension?.toggle() + + if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) { + Actions.restartPhpFpm() + } + } + } + + @objc func openPhpInfo() { + var url: URL? + + asyncWithBusyUI { + url = Actions.createTempPhpInfoFile() + } completion: { + if url != nil { NSWorkspace.shared.open(url!) } + } + } + + @objc func updateGlobalComposerDependencies() { + ComposerWindow().updateGlobalDependencies( + notify: true, + completion: { _ in } + ) + } + + @objc func openActiveConfigFolder() { + if PhpEnv.phpInstall.version.error { + Actions.openGenericPhpConfigFolder() + return + } + + Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short) + } + + @objc func openGlobalComposerFolder() { + Actions.openGlobalComposerFolder() + } + + @objc func openValetConfigFolder() { + Actions.openValetConfigFolder() + } + + @objc func switchToPhpVersion(sender: PhpMenuItem) { + self.switchToPhpVersion(sender.version) + } + + @objc func switchToPhpVersion(_ version: String) { + setBusyImage() + PhpEnv.shared.isBusy = true + PhpEnv.shared.delegate = self + PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version) + + DispatchQueue.global(qos: .userInitiated).async { [unowned self] in + updatePhpVersionInStatusBar() + rebuild() + PhpEnv.switcher.performSwitch( + to: version, + completion: { + PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version) + } + ) + } + } + +} diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 2f7d00b..1eab055 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -45,29 +45,22 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate Use `rebuild(async:)` to ensure the rebuilding happens in the background. */ private func rebuildMenu() { - // Create a new menu let menu = StatusMenu() - // Add the PHP versions (or error messages) menu.addPhpVersionMenuItems() menu.addItem(NSMenuItem.separator()) - // Add the possible actions menu.addPhpActionMenuItems() menu.addItem(NSMenuItem.separator()) - // Add Valet interactions menu.addValetMenuItems() menu.addItem(NSMenuItem.separator()) - // Add services menu.addRemainingMenuItems() menu.addItem(NSMenuItem.separator()) - // Add about & quit menu items menu.addCoreMenuItems() - // Make sure every item can be interacted with menu.items.forEach({ (item) in item.target = self }) @@ -165,202 +158,6 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate } } - // MARK: - Actions - - @objc func fixHomebrewPermissions() { - if !BetterAlert() - .withInformation( - title: "alert.fix_homebrew_permissions.title".localized, - subtitle: "alert.fix_homebrew_permissions.subtitle".localized, - description: "alert.fix_homebrew_permissions.desc".localized - ) - .withPrimary(text: "alert.fix_homebrew_permissions.ok".localized) - .withSecondary(text: "alert.fix_homebrew_permissions.cancel".localized) - .didSelectPrimary() { - return - } - - asyncExecution { - try Actions.fixHomebrewPermissions() - } success: { - BetterAlert() - .withInformation( - title: "alert.fix_homebrew_permissions_done.title".localized, - subtitle: "alert.fix_homebrew_permissions_done.subtitle".localized, - description: "alert.fix_homebrew_permissions_done.desc".localized - ) - .withPrimary(text: "OK") - .show() - } failure: { error in - BetterAlert.show(for: error as! HomebrewPermissionError) - } - } - - @objc func restartPhpFpm() { - asyncExecution { - Actions.restartPhpFpm() - } - } - - @objc func restartAllServices() { - asyncExecution { - Actions.restartDnsMasq() - Actions.restartPhpFpm() - Actions.restartNginx() - } success: { - DispatchQueue.main.async { - LocalNotification.send( - title: "notification.services_restarted".localized, - subtitle: "notification.services_restarted_desc".localized - ) - } - } - } - - @objc func stopAllServices() { - asyncExecution { - Actions.stopAllServices() - } success: { - DispatchQueue.main.async { - LocalNotification.send( - title: "notification.services_stopped".localized, - subtitle: "notification.services_stopped_desc".localized - ) - } - } - } - - @objc func restartNginx() { - asyncExecution { - Actions.restartNginx() - } - } - - @objc func restartDnsMasq() { - asyncExecution { - Actions.restartDnsMasq() - } - } - - @objc func disableAllXdebugModes() { - guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { - Log.info("xdebug.mode could not be found in any .ini file, aborting.") - return - } - - do { - try file.replace(key: "xdebug.mode", value: "off") - - Log.perf("Refreshing menu...") - MainMenu.shared.rebuild() - restartPhpFpm() - } catch { - Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") - } - } - - @objc func toggleXdebugMode(sender: XdebugMenuItem) { - Log.info("Switching Xdebug to mode: \(sender.mode)") - - guard let file = PhpEnv.shared.getConfigFile(forKey: "xdebug.mode") else { - Log.info("xdebug.mode could not be found in any .ini file, aborting.") - return - } - - do { - // Get the active modes - var modes = Xdebug.activeModes - // Set the new modes - if let index = modes.firstIndex(of: sender.mode) { - modes.remove(at: index) - } else { - modes.append(sender.mode) - } - - // Replace the xdebug mode - var newValue = modes.joined(separator: ",") - if newValue.isEmpty { - newValue = "off" - } - - try file.replace(key: "xdebug.mode", value: newValue) - - Log.perf("Refreshing menu...") - MainMenu.shared.rebuild() - restartPhpFpm() - } catch { - Log.err("There was an issue replacing `xdebug.mode` in \(file.filePath)") - } - } - - @objc func toggleExtension(sender: ExtensionMenuItem) { - asyncExecution { - sender.phpExtension?.toggle() - - if Preferences.isEnabled(.autoServiceRestartAfterExtensionToggle) { - Actions.restartPhpFpm() - } - } - } - - @objc func openPhpInfo() { - var url: URL? - - asyncWithBusyUI { - url = Actions.createTempPhpInfoFile() - } completion: { - if url != nil { NSWorkspace.shared.open(url!) } - } - } - - @objc func updateGlobalComposerDependencies() { - ComposerWindow().updateGlobalDependencies( - notify: true, - completion: { _ in } - ) - } - - @objc func openActiveConfigFolder() { - if PhpEnv.phpInstall.version.error { - // php version was not identified - Actions.openGenericPhpConfigFolder() - return - } - - // php version was identified - Actions.openPhpConfigFolder(version: PhpEnv.phpInstall.version.short) - } - - @objc func openGlobalComposerFolder() { - Actions.openGlobalComposerFolder() - } - - @objc func openValetConfigFolder() { - Actions.openValetConfigFolder() - } - - @objc func switchToPhpVersion(sender: PhpMenuItem) { - self.switchToPhpVersion(sender.version) - } - - @objc func switchToPhpVersion(_ version: String) { - setBusyImage() - PhpEnv.shared.isBusy = true - PhpEnv.shared.delegate = self - PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version) - - DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - updatePhpVersionInStatusBar() - rebuild() - PhpEnv.switcher.performSwitch( - to: version, - completion: { - PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version) - } - ) - } - } - // MARK: - Menu Item Functionality @objc func openAbout() { From e9f0d19d9a80caa8d6fc8f9bac37c45edf6da21a Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 19 May 2022 20:12:45 +0200 Subject: [PATCH 15/58] =?UTF-8?q?=F0=9F=94=A7=20Use=20dev=20icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index a01e18a..824f5e4 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -1543,7 +1543,7 @@ C41C1B4422B0098000E7CF16 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; @@ -1569,7 +1569,7 @@ C41C1B4522B0098000E7CF16 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIconDev; CODE_SIGN_ENTITLEMENTS = phpmon/phpmon.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; From 64491c6fe15acf4289cf28d099e3cc9d0947a274 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 29 May 2022 12:32:48 +0200 Subject: [PATCH 16/58] =?UTF-8?q?=E2=9C=A8=20Allow=20application=20of=20pr?= =?UTF-8?q?esets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/Menu/MainMenu+Actions.swift | 2 +- phpmon/Domain/Preferences/CustomPrefs.swift | 34 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift index 390c1c9..a66c86a 100644 --- a/phpmon/Domain/Menu/MainMenu+Actions.swift +++ b/phpmon/Domain/Menu/MainMenu+Actions.swift @@ -147,7 +147,7 @@ extension MainMenu { @objc func togglePreset(sender: PresetMenuItem) { asyncExecution { - dump(sender.preset) + sender.preset?.apply() } } diff --git a/phpmon/Domain/Preferences/CustomPrefs.swift b/phpmon/Domain/Preferences/CustomPrefs.swift index 4a5f47c..ffb1cd3 100644 --- a/phpmon/Domain/Preferences/CustomPrefs.swift +++ b/phpmon/Domain/Preferences/CustomPrefs.swift @@ -21,5 +21,39 @@ struct CustomPrefs: Decodable { let name: String let extensions: [String: Bool] let configuration: [String: String?] + + public func apply() { + // Apply the configuration changes first + for conf in configuration { + applyConfigurationValue(key: conf.key, value: conf.value ?? "") + } + + // Apply the extension changes in-place afterward + for ext in extensions { + for foundExt in PhpEnv.phpInstall.extensions + where foundExt.name == ext.key && foundExt.enabled != ext.value { + Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)") + foundExt.toggle() + break + } + } + + Actions.restartPhpFpm() + } + + private func applyConfigurationValue(key: String, value: String) { + guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { + return + } + + do { + if file.has(key: key) { + Log.info("Setting config value \(key) in \(file.filePath)") + try file.replace(key: key, value: value) + } + } catch { + Log.err("Setting \(key) to \(value) failed.") + } + } } } From 19aa804cbb33acdb0dac9572448dad41e66e0f3f Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 29 May 2022 22:30:11 +0200 Subject: [PATCH 17/58] =?UTF-8?q?=F0=9F=90=9B=20Alert=20user=20about=20iss?= =?UTF-8?q?ue=20#174?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- phpmon/Domain/App/Startup.swift | 15 +++++++++++++++ phpmon/Localizable.strings | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index 3c73208..2825640 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -184,6 +184,21 @@ class Startup { descriptionText: "startup.errors.valet_json_invalid.desc".localized ), // ================================================================================= + // Check for `which` alias issue + // ================================================================================= + EnvironmentCheck( + command: { + return App.architecture == "x86_64" + && 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, + subtitleText: "startup.errors.which_alias_issue.subtitle".localized, + descriptionText: "startup.errors.which_alias_issue.desc".localized + ), + // ================================================================================= // Determine the Valet version and ensure it isn't unknown. // ================================================================================= EnvironmentCheck( diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 98b6421..b39da0c 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -365,6 +365,11 @@ You can do this by running `composer global update` in your terminal. After that "startup.errors.services_json_error.subtitle" = "PHP Monitor usually queries `brew` using the following command to test if the services can be retrieved: `sudo brew services info nginx --json`.\n\nPHP Monitor could not interpret this response."; "startup.errors.services_json_error.desc" = "This can happen if your Homebrew installation is out of date, in which case Homebrew won't return JSON yet. You can usually fix this by running `brew update`. You can also try running `sudo brew services info nginx --json` in your terminal of choice."; +/// Issue with `which` alias +"startup.errors.which_alias_issue.title" = "A configuration issue was detected"; +"startup.errors.which_alias_issue.subtitle" = "It appears that there's a file in `/usr/local/bin/which`. This is usually set up by NodeJS, but `node` isn't in the PATH in `/usr/local/bin`. To fix this, keep reading."; +"startup.errors.which_alias_issue.desc" = "You will need to symlink `node` into the `/usr/local/bin` directory to make sure PHP Monitor can start successfully. For more info, see: https://github.com/nicoverbruggen/phpmon/issues/174"; + // SPONSOR ENCOURAGEMENT "startup.sponsor_encouragement.title" = "If PHP Monitor has been useful to you or your company, please consider leaving a tip."; From bbebe789970d3ed29ae8e451415d4329981219cc Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 30 May 2022 19:34:10 +0200 Subject: [PATCH 18/58] =?UTF-8?q?=E2=9C=A8=20Updated=20UI=20for=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 6 + .../Common/Extensions/NSMenuExtension.swift | 2 +- .../Common/Extensions/StringExtension.swift | 18 ++ phpmon/Domain/App/AppDelegate.swift | 2 +- phpmon/Domain/Menu/StatusMenu+Items.swift | 221 ++++++++++++++++++ phpmon/Domain/Menu/StatusMenu.swift | 196 +--------------- phpmon/Domain/Preferences/CustomPrefs.swift | 96 +++++--- phpmon/Localizable.strings | 7 + 8 files changed, 322 insertions(+), 226 deletions(-) create mode 100644 phpmon/Domain/Menu/StatusMenu+Items.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index d5f1c21..c14344b 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -88,6 +88,8 @@ C42337A3281F19F000459A48 /* Xdebug.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42337A2281F19F000459A48 /* Xdebug.swift */; }; C42759672627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; }; C42759682627662800093CAE /* NSMenuExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42759662627662800093CAE /* NSMenuExtension.swift */; }; + C42800AA28452AA10099C999 /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42800A928452AA10099C999 /* StatusMenu+Items.swift */; }; + C42800AB28452AA50099C999 /* StatusMenu+Items.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42800A928452AA10099C999 /* StatusMenu+Items.swift */; }; C42C49DB27C2806F0074ABAC /* MainMenu+FixMyValet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */; }; C42CFB1627DFDE7900862737 /* nginx-site.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1527DFDE7900862737 /* nginx-site.test */; }; C42CFB1827DFDFDC00862737 /* nginx-site-isolated.test in Resources */ = {isa = PBXBuildFile; fileRef = C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */; }; @@ -334,6 +336,7 @@ C4232EE42612526500158FC6 /* Credits.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = Credits.html; sourceTree = ""; }; C42337A2281F19F000459A48 /* Xdebug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xdebug.swift; sourceTree = ""; }; C42759662627662800093CAE /* NSMenuExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSMenuExtension.swift; sourceTree = ""; }; + C42800A928452AA10099C999 /* StatusMenu+Items.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusMenu+Items.swift"; sourceTree = ""; }; C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+FixMyValet.swift"; sourceTree = ""; }; C42CFB1527DFDE7900862737 /* nginx-site.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site.test"; sourceTree = ""; }; C42CFB1727DFDFDC00862737 /* nginx-site-isolated.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-site-isolated.test"; sourceTree = ""; }; @@ -754,6 +757,7 @@ C42C49DA27C2806F0074ABAC /* MainMenu+FixMyValet.swift */, C4F361602836BFD9003598CC /* MainMenu+Actions.swift */, C47331A1247093B7009A0597 /* StatusMenu.swift */, + C42800A928452AA10099C999 /* StatusMenu+Items.swift */, C48D0C9525CC80B100CC7490 /* HeaderView.swift */, C48D0C9925CC888B00CC7490 /* HeaderView.xib */, C48D0CA225CC992000CC7490 /* StatsView.swift */, @@ -1211,6 +1215,7 @@ C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */, C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */, + C42800AA28452AA10099C999 /* StatusMenu+Items.swift in Sources */, C415D3B72770F294005EF286 /* Actions.swift in Sources */, C4AC51FC27E27F47008528CA /* DomainListKindCell.swift in Sources */, C4F361612836BFD9003598CC /* MainMenu+Actions.swift in Sources */, @@ -1349,6 +1354,7 @@ C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */, C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */, + C42800AB28452AA50099C999 /* StatusMenu+Items.swift in Sources */, C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */, C415D3E92770F692005EF286 /* AppDelegate+InterApp.swift in Sources */, C484437C2804BB560041A78A /* ValetProxyScanner.swift in Sources */, diff --git a/phpmon/Common/Extensions/NSMenuExtension.swift b/phpmon/Common/Extensions/NSMenuExtension.swift index 93b90c6..105beaa 100644 --- a/phpmon/Common/Extensions/NSMenuExtension.swift +++ b/phpmon/Common/Extensions/NSMenuExtension.swift @@ -47,5 +47,5 @@ class EditorMenuItem: NSMenuItem { } class PresetMenuItem: NSMenuItem { - var preset: CustomPrefs.Preset? + var preset: Preset? } diff --git a/phpmon/Common/Extensions/StringExtension.swift b/phpmon/Common/Extensions/StringExtension.swift index 74702df..a440886 100644 --- a/phpmon/Common/Extensions/StringExtension.swift +++ b/phpmon/Common/Extensions/StringExtension.swift @@ -71,4 +71,22 @@ extension String { } } + var stripped: String { + do { + guard let data = self.data(using: .unicode) else { + return "" + } + let attributed = try NSAttributedString( + data: data, + options: [ + .documentType: NSAttributedString.DocumentType.html, + .characterEncoding: String.Encoding.utf8.rawValue], + documentAttributes: nil + ) + return attributed.string + } catch { + return "" + } + } + } diff --git a/phpmon/Domain/App/AppDelegate.swift b/phpmon/Domain/App/AppDelegate.swift index c455fff..96f5628 100644 --- a/phpmon/Domain/App/AppDelegate.swift +++ b/phpmon/Domain/App/AppDelegate.swift @@ -65,7 +65,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele override init() { logger.verbosity = .info #if DEBUG - logger.verbosity = .performance + // logger.verbosity = .performance #endif if CommandLine.arguments.contains("--v") { logger.verbosity = .performance diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift new file mode 100644 index 0000000..302181d --- /dev/null +++ b/phpmon/Domain/Menu/StatusMenu+Items.swift @@ -0,0 +1,221 @@ +// +// StatusMenu+Items.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 30/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Cocoa + +extension StatusMenu { + + // MARK: Remaining Menu Items + + func addConfigurationMenuItems() { + self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized)) + self.addItem( + NSMenuItem(title: "mi_php_config".localized, + action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c") + ) + self.addItem( + NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i") + ) + } + + func addComposerMenuItems() { + self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized)) + self.addItem( + NSMenuItem(title: "mi_global_composer".localized, + action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g") + ) + + let composerMenuItem = NSMenuItem( + title: "mi_update_global_composer".localized, + action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), + keyEquivalent: "g" + ) + composerMenuItem.keyEquivalentModifierMask = .shift + + self.addItem(composerMenuItem) + } + + func addStatsMenuItem() { + guard let stats = PhpEnv.phpInstall.limits else { return } + + self.addItem(StatsView.asMenuItem( + memory: stats.memory_limit, + post: stats.post_max_size, + upload: stats.upload_max_filesize) + ) + } + + func addExtensionsMenuItems() { + self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized)) + + if PhpEnv.phpInstall.extensions.isEmpty { + self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: "")) + } + + var shortcutKey = 1 + for phpExtension in PhpEnv.phpInstall.extensions { + self.addExtensionItem(phpExtension, shortcutKey) + shortcutKey += 1 + } + } + + func addPresetsMenuItem() { + if Preferences.custom.presets.isEmpty { + return + } + + let presets = NSMenuItem(title: "Configuration Presets", action: nil, keyEquivalent: "") + let presetsMenu = NSMenu() + presetsMenu.addItem(NSMenuItem.separator()) + presetsMenu.addItem(HeaderView.asMenuItem(text: "Apply Configuration Presets")) + + for preset in Preferences.custom.presets { + let presetMenuItem = PresetMenuItem( + title: preset.getMenuItemText(), + action: #selector(MainMenu.togglePreset(sender:)), + keyEquivalent: "" + ) + + if let attributedString = try? NSMutableAttributedString( + data: preset.getMenuItemText().data(using: .utf8)!, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil + ) { + /* + attributedString.addAttribute( + .font, + value: NSFont.systemFont(ofSize: 12), + range: NSRange(location: 0, length: attributedString.length) + ) + */ + presetMenuItem.attributedTitle = attributedString + } + + presetMenuItem.preset = preset + presetsMenu.addItem(presetMenuItem) + } + + presetsMenu.addItem(NSMenuItem.separator()) + presetsMenu.addItem(NSMenuItem( + title: "Revert to Previous Configuration...", + action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "") + ) + presetsMenu.addItem(NSMenuItem.separator()) + presetsMenu.addItem(NSMenuItem( + title: "\(Preferences.custom.presets.count) profiles loaded from configuration file", + action: nil, keyEquivalent: "") + ) + for item in presetsMenu.items { + item.target = MainMenu.shared + } + self.setSubmenu(presetsMenu, for: presets) + self.addItem(presets) + } + + func addXdebugMenuItem() { + if !Xdebug.enabled { + return + } + + let xdebugSwitch = NSMenuItem( + title: "mi_xdebug_mode".localized, + action: nil, + keyEquivalent: "" + ) + let xdebugModesMenu = NSMenu() + let activeModes = Xdebug.activeModes + + xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "Available Modes")) + + for mode in Xdebug.modes { + let item = XdebugMenuItem( + title: mode, + action: #selector(MainMenu.toggleXdebugMode(sender:)), + keyEquivalent: "" + ) + + item.state = activeModes.contains(mode) ? .on : .off + item.mode = mode + xdebugModesMenu.addItem(item) + } + + xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "Actions")) + xdebugModesMenu.addItem( + withTitle: "Disable All", + action: #selector(MainMenu.disableAllXdebugModes), + keyEquivalent: "" + ) + + for item in xdebugModesMenu.items { + item.target = MainMenu.shared + } + + self.setSubmenu(xdebugModesMenu, for: xdebugSwitch) + self.addItem(xdebugSwitch) + } + + func addFirstAidAndServicesMenuItems() { + let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "") + let servicesMenu = NSMenu() + + let fixMyValetMenuItem = NSMenuItem( + title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion), + action: #selector(MainMenu.fixMyValet), keyEquivalent: "" + ) + fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized + servicesMenu.addItem(fixMyValetMenuItem) + + let fixHomebrewMenuItem = NSMenuItem( + title: "mi_fix_brew_permissions".localized(), + action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: "" + ) + fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized + servicesMenu.addItem(fixHomebrewMenuItem) + + servicesMenu.addItem(NSMenuItem.separator()) + servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized)) + + servicesMenu.addItem( + NSMenuItem(title: "mi_restart_dnsmasq".localized, + action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d") + ) + servicesMenu.addItem( + NSMenuItem(title: "mi_restart_php_fpm".localized, + action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p") + ) + servicesMenu.addItem( + NSMenuItem(title: "mi_restart_nginx".localized, + action: #selector(MainMenu.restartNginx), keyEquivalent: "n") + ) + servicesMenu.addItem( + NSMenuItem(title: "mi_restart_all_services".localized, + action: #selector(MainMenu.restartAllServices), keyEquivalent: "s") + ) + servicesMenu.addItem( + NSMenuItem(title: "mi_stop_all_services".localized, + action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"), + withKeyModifier: [.command, .shift] + ) + + servicesMenu.addItem(NSMenuItem.separator()) + servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized)) + + servicesMenu.addItem( + NSMenuItem(title: "mi_php_refresh".localized, + action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r") + ) + + for item in servicesMenu.items { + item.target = MainMenu.shared + } + + self.setSubmenu(servicesMenu, for: services) + self.addItem(services) + } + +} diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index 9880b6c..5da271e 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -87,201 +87,9 @@ class StatusMenu: NSMenu { action: #selector(MainMenu.terminateApp), keyEquivalent: "q")) } - // MARK: Remaining Menu Items - - func addConfigurationMenuItems() { - self.addItem(HeaderView.asMenuItem(text: "mi_configuration".localized)) - self.addItem( - NSMenuItem(title: "mi_php_config".localized, - action: #selector(MainMenu.openActiveConfigFolder), keyEquivalent: "c") - ) - self.addItem( - NSMenuItem(title: "mi_phpinfo".localized, action: #selector(MainMenu.openPhpInfo), keyEquivalent: "i") - ) - } - - func addComposerMenuItems() { - self.addItem(HeaderView.asMenuItem(text: "mi_composer".localized)) - self.addItem( - NSMenuItem(title: "mi_global_composer".localized, - action: #selector(MainMenu.openGlobalComposerFolder), keyEquivalent: "g") - ) - - let composerMenuItem = NSMenuItem( - title: "mi_update_global_composer".localized, - action: PhpEnv.shared.isBusy ? nil : #selector(MainMenu.updateGlobalComposerDependencies), - keyEquivalent: "g" - ) - composerMenuItem.keyEquivalentModifierMask = .shift - - self.addItem(composerMenuItem) - } - - func addStatsMenuItem() { - guard let stats = PhpEnv.phpInstall.limits else { return } - - self.addItem(StatsView.asMenuItem( - memory: stats.memory_limit, - post: stats.post_max_size, - upload: stats.upload_max_filesize) - ) - } - - func addExtensionsMenuItems() { - self.addItem(HeaderView.asMenuItem(text: "mi_detected_extensions".localized)) - - if PhpEnv.phpInstall.extensions.isEmpty { - self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: "")) - } - - var shortcutKey = 1 - for phpExtension in PhpEnv.phpInstall.extensions { - self.addExtensionItem(phpExtension, shortcutKey) - shortcutKey += 1 - } - } - - func addPresetsMenuItem() { - if Preferences.custom.presets.isEmpty { - return - } - - let presets = NSMenuItem(title: "Configuration Presets", action: nil, keyEquivalent: "") - let presetsMenu = NSMenu() - presetsMenu.addItem(NSMenuItem.separator()) - presetsMenu.addItem(HeaderView.asMenuItem(text: "Apply Configuration Presets")) - - for preset in Preferences.custom.presets { - let presetMenuItem = PresetMenuItem( - title: "\(preset.name) (\(preset.extensions.count) extension, \(preset.configuration.count) prefs)", - action: #selector(MainMenu.togglePreset(sender:)), - keyEquivalent: "" - ) - presetMenuItem.preset = preset - presetsMenu.addItem(presetMenuItem) - } - - presetsMenu.addItem(NSMenuItem.separator()) - presetsMenu.addItem(NSMenuItem( - title: "Revert to Previous Configuration...", - action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "") - ) - presetsMenu.addItem(NSMenuItem.separator()) - presetsMenu.addItem(NSMenuItem( - title: "\(Preferences.custom.presets.count) profiles loaded from configuration file", - action: nil, keyEquivalent: "") - ) - for item in presetsMenu.items { - item.target = MainMenu.shared - } - self.setSubmenu(presetsMenu, for: presets) - self.addItem(presets) - } - - func addXdebugMenuItem() { - if !Xdebug.enabled { - return - } - - let xdebugSwitch = NSMenuItem( - title: "mi_xdebug_mode".localized, - action: nil, - keyEquivalent: "" - ) - let xdebugModesMenu = NSMenu() - let activeModes = Xdebug.activeModes - - xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "Available Modes")) - - for mode in Xdebug.modes { - let item = XdebugMenuItem( - title: mode, - action: #selector(MainMenu.toggleXdebugMode(sender:)), - keyEquivalent: "" - ) - - item.state = activeModes.contains(mode) ? .on : .off - item.mode = mode - xdebugModesMenu.addItem(item) - } - - xdebugModesMenu.addItem(HeaderView.asMenuItem(text: "Actions")) - xdebugModesMenu.addItem( - withTitle: "Disable All", - action: #selector(MainMenu.disableAllXdebugModes), - keyEquivalent: "" - ) - - for item in xdebugModesMenu.items { - item.target = MainMenu.shared - } - - self.setSubmenu(xdebugModesMenu, for: xdebugSwitch) - self.addItem(xdebugSwitch) - } - - func addFirstAidAndServicesMenuItems() { - let services = NSMenuItem(title: "mi_other".localized, action: nil, keyEquivalent: "") - let servicesMenu = NSMenu() - - let fixMyValetMenuItem = NSMenuItem( - title: "mi_fix_my_valet".localized(PhpEnv.brewPhpVersion), - action: #selector(MainMenu.fixMyValet), keyEquivalent: "" - ) - fixMyValetMenuItem.toolTip = "mi_fix_my_valet_tooltip".localized - servicesMenu.addItem(fixMyValetMenuItem) - - let fixHomebrewMenuItem = NSMenuItem( - title: "mi_fix_brew_permissions".localized(), - action: #selector(MainMenu.fixHomebrewPermissions), keyEquivalent: "" - ) - fixHomebrewMenuItem.toolTip = "mi_fix_brew_permissions_tooltip".localized - servicesMenu.addItem(fixHomebrewMenuItem) - - servicesMenu.addItem(NSMenuItem.separator()) - servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_services".localized)) - - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_dnsmasq".localized, - action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "d") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_php_fpm".localized, - action: #selector(MainMenu.restartPhpFpm), keyEquivalent: "p") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_nginx".localized, - action: #selector(MainMenu.restartNginx), keyEquivalent: "n") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_restart_all_services".localized, - action: #selector(MainMenu.restartAllServices), keyEquivalent: "s") - ) - servicesMenu.addItem( - NSMenuItem(title: "mi_stop_all_services".localized, - action: #selector(MainMenu.stopAllServices), keyEquivalent: "s"), - withKeyModifier: [.command, .shift] - ) - - servicesMenu.addItem(NSMenuItem.separator()) - servicesMenu.addItem(HeaderView.asMenuItem(text: "mi_manual_actions".localized)) - - servicesMenu.addItem( - NSMenuItem(title: "mi_php_refresh".localized, - action: #selector(MainMenu.reloadPhpMonitorMenuInForeground), keyEquivalent: "r") - ) - - for item in servicesMenu.items { - item.target = MainMenu.shared - } - - self.setSubmenu(servicesMenu, for: services) - self.addItem(services) - } - // MARK: Private Helpers - private func addSwitchToPhpMenuItems() { + internal func addSwitchToPhpMenuItems() { var shortcutKey = 1 for index in (0.. String { + var info = extensions.count == 1 + ? "preset.extension".localized(extensions.count) + : "preset.extensions".localized(extensions.count) + info += ", " + info += configuration.count == 1 + ? "preset.preference".localized(configuration.count) + : "preset.preferences".localized(configuration.count) - Actions.restartPhpFpm() + if self.version == nil || !PhpEnv.shared.availablePhpVersions.contains(self.version!) { + return "" + + "\(name.stripped)
" + + "" + + info + "" + + "
" } - private func applyConfigurationValue(key: String, value: String) { - guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { - return - } + return "" + + "\(name.stripped)
" + + "" + + "Switches to PHP \(version!)
" + + info + "
" + + "
" + } - do { - if file.has(key: key) { - Log.info("Setting config value \(key) in \(file.filePath)") - try file.replace(key: key, value: value) - } - } catch { - Log.err("Setting \(key) to \(value) failed.") + public func apply() { + // Apply the PHP version if is considered a valid version + // TODO + + // Apply the configuration changes first + for conf in configuration { + applyConfigurationValue(key: conf.key, value: conf.value ?? "") + } + + // Apply the extension changes in-place afterward + for ext in extensions { + for foundExt in PhpEnv.phpInstall.extensions + where foundExt.name == ext.key && foundExt.enabled != ext.value { + Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)") + foundExt.toggle() + break } } + + Actions.restartPhpFpm() + } + + private func applyConfigurationValue(key: String, value: String) { + guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { + return + } + + do { + if file.has(key: key) { + Log.info("Setting config value \(key) in \(file.filePath)") + try file.replace(key: key, value: value) + } + } catch { + Log.err("Setting \(key) to \(value) failed.") + } } } diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index b39da0c..f16d0cf 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -167,6 +167,13 @@ "driver.not_detected" = "Other"; +// PRESET + +"preset.extension" = "%i extension"; +"preset.extensions" = "%i extensions"; +"preset.preference" = "%i preference"; +"preset.preferences" = "%i preferences"; + // EDITORS "editors.alert.try_again" = "Try Again"; From da8659adba9b425acf81ba26a096e38543a417fb Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Tue, 31 May 2022 21:43:24 +0200 Subject: [PATCH 19/58] =?UTF-8?q?=E2=9C=A8=20Enable=20version=20switching?= =?UTF-8?q?=20in=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Moved Preset to dedicated file * Added async friendly PHP version switch * Added conditional PHP switch based on Preset --- PHP Monitor.xcodeproj/project.pbxproj | 14 +++ phpmon/Domain/Menu/MainMenu+Actions.swift | 38 +++++++ phpmon/Domain/Menu/MainMenu+Switcher.swift | 8 +- phpmon/Domain/Preferences/CustomPrefs.swift | 76 ------------- phpmon/Domain/Presets/Preset.swift | 116 ++++++++++++++++++++ 5 files changed, 169 insertions(+), 83 deletions(-) create mode 100644 phpmon/Domain/Presets/Preset.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index c14344b..e040153 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -48,6 +48,8 @@ C40B24F127A3106D0018C7D2 /* ServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E67279DE0540010F296 /* ServicesView.swift */; }; C40B24F227A310770018C7D2 /* Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EC1E72279DFCF40010F296 /* Events.swift */; }; C40B24F427A310830018C7D2 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; + C40C5C9C2846A40600E28255 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; + C40C5C9D2846A40600E28255 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C5C9B2846A40600E28255 /* Preset.swift */; }; C40C7F1E2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; C40C7F1F2772136000DDDCDC /* PhpEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F1D2772136000DDDCDC /* PhpEnv.swift */; }; C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */ = {isa = PBXBuildFile; fileRef = C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */; }; @@ -307,6 +309,7 @@ C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIcons.swift; sourceTree = ""; }; C4080FF527BD8C6400BF2C6B /* BetterAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlert.swift; sourceTree = ""; }; C4080FF927BD956700BF2C6B /* BetterAlertVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BetterAlertVC.swift; sourceTree = ""; }; + C40C5C9B2846A40600E28255 /* Preset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preset.swift; sourceTree = ""; }; C40C7F1D2772136000DDDCDC /* PhpEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpEnv.swift; sourceTree = ""; }; C40C7F2727721FF600DDDCDC /* ActivePhpInstallation+Checks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ActivePhpInstallation+Checks.swift"; sourceTree = ""; }; C40C7F2F27722E8D00DDDCDC /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; @@ -554,6 +557,14 @@ path = Notice; sourceTree = ""; }; + C40C5C9E2846A42D00E28255 /* Presets */ = { + isa = PBXGroup; + children = ( + C40C5C9B2846A40600E28255 /* Preset.swift */, + ); + path = Presets; + sourceTree = ""; + }; C40C7F1C27720E1400DDDCDC /* Test Files */ = { isa = PBXGroup; children = ( @@ -643,6 +654,7 @@ 5420395726135DB800FB00FA /* Preferences */, C44C198F276E3A380072762D /* Progress */, C4C8E81D276F5686003AC782 /* Watcher */, + C40C5C9E2846A42D00E28255 /* Presets */, C4EE55B027708BB2001DF387 /* SwiftUI */, ); path = Domain; @@ -1212,6 +1224,7 @@ C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */, C4B585442770FE3900DA4FBE /* Command.swift in Sources */, C44067F527E2582B0045BD4E /* DomainListNameCell.swift in Sources */, + C40C5C9C2846A40600E28255 /* Preset.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, C4EE55AB27708B9E001DF387 /* Preview.swift in Sources */, C44067F727E258410045BD4E /* DomainListPhpCell.swift in Sources */, @@ -1353,6 +1366,7 @@ C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */, C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, + C40C5C9D2846A40600E28255 /* Preset.swift in Sources */, C4F30B09278E1A0E00755FCE /* CustomPrefs.swift in Sources */, C42800AB28452AA50099C999 /* StatusMenu+Items.swift in Sources */, C40FE738282ABA4F00A302C2 /* AppVersion.swift in Sources */, diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift index a66c86a..7ee6502 100644 --- a/phpmon/Domain/Menu/MainMenu+Actions.swift +++ b/phpmon/Domain/Menu/MainMenu+Actions.swift @@ -201,10 +201,48 @@ extension MainMenu { PhpEnv.switcher.performSwitch( to: version, completion: { + PhpEnv.shared.currentInstall = ActivePhpInstallation() + App.shared.handlePhpConfigWatcher() PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version) } ) } } + // MARK: - Async + + /** + This async-friendly version of the switcher can be invoked elsewhere in the app: + ``` + Task { + await MainMenu.shared.switchToPhp("8.1") + // thing to do after the switch + } + ``` + Since this async function uses `withCheckedContinuation` + any code after will run only after the switcher is done. + */ + func switchToPhp(_ version: String) async { + DispatchQueue.main.async { [self] in + setBusyImage() + PhpEnv.shared.isBusy = true + PhpEnv.shared.delegate = self + PhpEnv.shared.delegate?.switcherDidStartSwitching(to: version) + } + + return await withCheckedContinuation({ continuation in + updatePhpVersionInStatusBar() + rebuild() + PhpEnv.switcher.performSwitch( + to: version, + completion: { + PhpEnv.shared.currentInstall = ActivePhpInstallation() + App.shared.handlePhpConfigWatcher() + PhpEnv.shared.delegate?.switcherDidCompleteSwitch(to: version) + continuation.resume() + } + ) + }) + } + } diff --git a/phpmon/Domain/Menu/MainMenu+Switcher.swift b/phpmon/Domain/Menu/MainMenu+Switcher.swift index 8134a8e..3100d20 100644 --- a/phpmon/Domain/Menu/MainMenu+Switcher.swift +++ b/phpmon/Domain/Menu/MainMenu+Switcher.swift @@ -15,12 +15,6 @@ extension MainMenu { func switcherDidStartSwitching(to version: String) {} func switcherDidCompleteSwitch(to version: String) { - // Update the PHP version - PhpEnv.shared.currentInstall = ActivePhpInstallation() - - // Ensure the config watcher gets reloaded - App.shared.handlePhpConfigWatcher() - // Mark as no longer busy PhpEnv.shared.isBusy = false @@ -56,7 +50,7 @@ extension MainMenu { } } - @MainActor private func suggestFixMyValet(failed version: String) { + private func suggestFixMyValet(failed version: String) { let outcome = BetterAlert() .withInformation( title: "alert.php_switch_failed.title".localized(version), diff --git a/phpmon/Domain/Preferences/CustomPrefs.swift b/phpmon/Domain/Preferences/CustomPrefs.swift index 0fe0c03..03d6db6 100644 --- a/phpmon/Domain/Preferences/CustomPrefs.swift +++ b/phpmon/Domain/Preferences/CustomPrefs.swift @@ -17,79 +17,3 @@ struct CustomPrefs: Decodable { case presets = "presets" } } - -struct Preset: Decodable { - let name: String - let version: String? - let extensions: [String: Bool] - let configuration: [String: String?] - - public enum CodingKeys: String, CodingKey { - case version = "php", - name = "name", - extensions = "extensions", - configuration = "configuration" - } - - public func getMenuItemText() -> String { - var info = extensions.count == 1 - ? "preset.extension".localized(extensions.count) - : "preset.extensions".localized(extensions.count) - info += ", " - info += configuration.count == 1 - ? "preset.preference".localized(configuration.count) - : "preset.preferences".localized(configuration.count) - - if self.version == nil || !PhpEnv.shared.availablePhpVersions.contains(self.version!) { - return "" - + "\(name.stripped)
" - + "" - + info + "" - + "
" - } - - return "" - + "\(name.stripped)
" - + "" - + "Switches to PHP \(version!)
" - + info + "
" - + "
" - } - - public func apply() { - // Apply the PHP version if is considered a valid version - // TODO - - // Apply the configuration changes first - for conf in configuration { - applyConfigurationValue(key: conf.key, value: conf.value ?? "") - } - - // Apply the extension changes in-place afterward - for ext in extensions { - for foundExt in PhpEnv.phpInstall.extensions - where foundExt.name == ext.key && foundExt.enabled != ext.value { - Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)") - foundExt.toggle() - break - } - } - - Actions.restartPhpFpm() - } - - private func applyConfigurationValue(key: String, value: String) { - guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { - return - } - - do { - if file.has(key: key) { - Log.info("Setting config value \(key) in \(file.filePath)") - try file.replace(key: key, value: value) - } - } catch { - Log.err("Setting \(key) to \(value) failed.") - } - } -} diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift new file mode 100644 index 0000000..62f71b0 --- /dev/null +++ b/phpmon/Domain/Presets/Preset.swift @@ -0,0 +1,116 @@ +// +// Preset.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 31/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +struct Preset: Decodable { + let name: String + let version: String? + let extensions: [String: Bool] + let configuration: [String: String?] + + public enum CodingKeys: String, CodingKey { + case version = "php", + name = "name", + extensions = "extensions", + configuration = "configuration" + } + + public func getMenuItemText() -> String { + var info = extensions.count == 1 + ? "preset.extension".localized(extensions.count) + : "preset.extensions".localized(extensions.count) + info += ", " + info += configuration.count == 1 + ? "preset.preference".localized(configuration.count) + : "preset.preferences".localized(configuration.count) + + if self.version == nil { + return "" + + "\(name.stripped)
" + + "" + + info + "" + + "
" + } + + return "" + + "\(name.stripped)
" + + "" + + "Switches to PHP \(version!)
" + + info + "
" + + "
" + } + + public func apply() { + Task { + // Apply the PHP version if is considered a valid version + if self.version != nil { + await switchToPhpVersionIfValid() + } + + // Apply the configuration changes first + for conf in configuration { + applyConfigurationValue(key: conf.key, value: conf.value ?? "") + } + + // Apply the extension changes in-place afterward + for ext in extensions { + for foundExt in PhpEnv.phpInstall.extensions + where foundExt.name == ext.key && foundExt.enabled != ext.value { + Log.info("Toggling extension \(foundExt.name) in \(foundExt.file)") + foundExt.toggle() + break + } + } + + Actions.restartPhpFpm() + } + } + + private func switchToPhpVersionIfValid() async { + if PhpEnv.shared.currentInstall.version.short == self.version! { + Log.info("The version we are supposed to switch to is already active.") + return + } + + if PhpEnv.shared.availablePhpVersions.first(where: { $0 == self.version }) != nil { + await MainMenu.shared.switchToPhp(self.version!) + return + } else { + DispatchQueue.main.async { + BetterAlert() + .withInformation( + title: "PHP version unavailable", + subtitle: "You have specified a PHP version (\(version!)) that is unavailable.", + description: "Please make sure this version of PHP is installed " + + "and you can switch to it in the dropdown. " + + "Currently supported versions include: " + + "\(PhpEnv.shared.availablePhpVersions.joined(separator: ", "))." + ) + .withPrimary(text: "OK") + .show() + } + return + } + } + + private func applyConfigurationValue(key: String, value: String) { + guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { + return + } + + do { + if file.has(key: key) { + Log.info("Setting config value \(key) in \(file.filePath)") + try file.replace(key: key, value: value) + } + } catch { + Log.err("Setting \(key) to \(value) failed.") + } + } +} From 0c09e808bdc122f45461e243432e5cd677b517a8 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 1 Jun 2022 12:44:22 +0200 Subject: [PATCH 20/58] =?UTF-8?q?=F0=9F=93=9D=20Test=20dark=20mode=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++++- docs/notification-dark.png | Bin 0 -> 18643 bytes 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 docs/notification-dark.png diff --git a/README.md b/README.md index ad385c2..b564d5c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,11 @@ It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)! -phpmon screenshot (notification) + + + phpmon screenshot (notification) + + PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more). diff --git a/docs/notification-dark.png b/docs/notification-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9cce2e91693867a0340a195346e1ba65617069ce GIT binary patch literal 18643 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%0DG*-NgyBJ@bd0n@v^M-A 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 M-Lc22jznJj8~F@4z5oCK literal 0 HcmV?d00001 From 86d74619b1431ea07630089244826fa386c152ac Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 1 Jun 2022 12:47:54 +0200 Subject: [PATCH 21/58] =?UTF-8?q?=F0=9F=93=9D=20Use=20non-standard=20way?= =?UTF-8?q?=20to=20add=20dark=20images?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b564d5c..98aaeca 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,8 @@ It's super convenient to switch between different versions of PHP. You'll even get notifications (only if you choose to opt-in, of course)! - - - phpmon screenshot (notification) - - +phpmon screenshot (notification) +phpmon screenshot (notification) PHP Monitor also gives you quick access to various useful functionality (like accessing configuration files, restarting services, and more). From 5907d9f689db83170fc4b58ce63ce73f32861ab3 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 1 Jun 2022 21:01:18 +0200 Subject: [PATCH 22/58] =?UTF-8?q?=E2=9C=A8=20Build=20revertable=20preset?= =?UTF-8?q?=20(#175)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For any given preset, we need to be able to determine what we'd need to do in order to revert the preset. That means figuring out what the diff is between the current PHP setup and what the preset would change. We'll persist that to its own preset, which can be reapplied if needed. The "revertable" preset is persisted to the following file: ~/.config/phpmon/preset_revert.json If that file is present and valid, the app should enable the 'revert' option. (That still needs to be implemented.) --- phpmon/Domain/Presets/Preset.swift | 139 +++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift index 62f71b0..f89e665 100644 --- a/phpmon/Domain/Presets/Preset.swift +++ b/phpmon/Domain/Presets/Preset.swift @@ -8,7 +8,7 @@ import Foundation -struct Preset: Decodable { +struct Preset: Codable { let name: String let version: String? let extensions: [String: Bool] @@ -21,33 +21,16 @@ struct Preset: Decodable { configuration = "configuration" } - public func getMenuItemText() -> String { - var info = extensions.count == 1 - ? "preset.extension".localized(extensions.count) - : "preset.extensions".localized(extensions.count) - info += ", " - info += configuration.count == 1 - ? "preset.preference".localized(configuration.count) - : "preset.preferences".localized(configuration.count) - - if self.version == nil { - return "" - + "\(name.stripped)
" - + "" - + info + "" - + "
" - } - - return "" - + "\(name.stripped)
" - + "" - + "Switches to PHP \(version!)
" - + info + "
" - + "
" - } + // MARK: Applying + /** + Applies a given preset. + */ public func apply() { Task { + // Save the preset that would revert this preset + self.persistRevert() + // Apply the PHP version if is considered a valid version if self.version != nil { await switchToPhpVersionIfValid() @@ -72,6 +55,8 @@ struct Preset: Decodable { } } + // MARK: - Apply Functionality + private func switchToPhpVersionIfValid() async { if PhpEnv.shared.currentInstall.version.short == self.version! { Log.info("The version we are supposed to switch to is already active.") @@ -113,4 +98,108 @@ struct Preset: Decodable { Log.err("Setting \(key) to \(value) failed.") } } + + // MARK: - Menu Items + + public func getMenuItemText() -> String { + var info = extensions.count == 1 + ? "preset.extension".localized(extensions.count) + : "preset.extensions".localized(extensions.count) + info += ", " + info += configuration.count == 1 + ? "preset.preference".localized(configuration.count) + : "preset.preferences".localized(configuration.count) + + if self.version == nil { + return "" + + "\(name.stripped)
" + + "" + + info + "" + + "
" + } + + return "" + + "\(name.stripped)
" + + "" + + "Switches to PHP \(version!)
" + + info + "
" + + "
" + } + + // MARK: - Reverting + + public var revertSnapshot: Preset { + return Preset( + name: "Revert", + version: diffVersion(), + extensions: diffExtensions(), + configuration: diffConfiguration() + ) + } + + /** + Returns the version that was previously active, which would revert this preset's version. + Returns nil if the version is not specified or the same. + */ + private func diffVersion() -> String? { + guard let version = self.version else { + return nil + } + + if PhpEnv.shared.currentInstall.version.short != version { + return PhpEnv.shared.currentInstall.version.short + } else { + return nil + } + } + + /** + Returns a list of extensions which would revert this presets's setup. + */ + private func diffExtensions() -> [String: Bool] { + var items: [String: Bool] = [:] + + for (key, value) in self.extensions { + for foundExt in PhpEnv.phpInstall.extensions + where foundExt.name == key && foundExt.enabled != value { + // Save the original value of the extension + items[foundExt.name] = foundExt.enabled + } + } + + return items + } + + /** + Returns a list of configuration items which would revert this presets's setup. + */ + private func diffConfiguration() -> [String: String?] { + var items: [String: String?] = [:] + + for (key, _) in self.configuration { + guard let file = PhpEnv.shared.getConfigFile(forKey: key) else { + break + } + + items[key] = file.get(for: key) + } + + return items + } + + /** + Persists the revert as a JSON file, so it can be read from a file after restarting PHP Monitor. + */ + private func persistRevert() { + let data = try! JSONEncoder().encode(self.revertSnapshot) + + Shell.run("mkdir -p ~/.config/phpmon") + + try! String(data: data, encoding: .utf8)! + .write( + toFile: "/Users/\(Paths.whoami)/.config/phpmon/preset_revert.json", + atomically: true, + encoding: .utf8 + ) + } } From 29b4fe2962d82f5912a22fc81b2f46bb2640756e Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Thu, 2 Jun 2022 20:31:01 +0200 Subject: [PATCH 23/58] =?UTF-8?q?=E2=9C=A8=20Load=20persisted=20revert=20&?= =?UTF-8?q?=20allow=20revert?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This also moves the location of the .phpmon.conf.json file to a new location: ~/.config/phpmon/config.json. --- PHP Monitor.xcodeproj/project.pbxproj | 6 ++++ README.md | 2 +- phpmon/Domain/Menu/MainMenu+Actions.swift | 8 +++++ phpmon/Domain/Menu/MainMenu+Startup.swift | 37 ++++++++++++--------- phpmon/Domain/Menu/StatusMenu+Items.swift | 7 ++-- phpmon/Domain/Preferences/Preferences.swift | 10 +++--- phpmon/Domain/Presets/Preset.swift | 4 +++ phpmon/Domain/Presets/PresetHelper.swift | 37 +++++++++++++++++++++ 8 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 phpmon/Domain/Presets/PresetHelper.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index e040153..eff0c4f 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -123,6 +123,8 @@ C44CCD4A27AFF3BC00CE40E5 /* MainMenu+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */; }; C44F868E2835BD8D005C353A /* phpmon-config.json in Resources */ = {isa = PBXBuildFile; fileRef = C44F868D2835BD8D005C353A /* phpmon-config.json */; }; C459B4BD27F6093700E9B4B4 /* nginx-proxy.test in Resources */ = {isa = PBXBuildFile; fileRef = C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */; }; + C463E380284930EE00422731 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; }; + C463E381284930EE00422731 /* PresetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C463E37F284930EE00422731 /* PresetHelper.swift */; }; C464ADAC275A7A3F003FCD53 /* DomainListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */; }; C464ADAD275A7A3F003FCD53 /* DomainListWC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */; }; C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; }; @@ -360,6 +362,7 @@ C44CCD4827AFF3B700CE40E5 /* MainMenu+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Async.swift"; sourceTree = ""; }; C44F868D2835BD8D005C353A /* phpmon-config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "phpmon-config.json"; sourceTree = ""; }; C459B4BC27F6093700E9B4B4 /* nginx-proxy.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-proxy.test"; sourceTree = ""; }; + C463E37F284930EE00422731 /* PresetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresetHelper.swift; sourceTree = ""; }; C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListWC.swift; sourceTree = ""; }; C464ADAE275A7A69003FCD53 /* DomainListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListVC.swift; sourceTree = ""; }; C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListCellProtocol.swift; sourceTree = ""; }; @@ -561,6 +564,7 @@ isa = PBXGroup; children = ( C40C5C9B2846A40600E28255 /* Preset.swift */, + C463E37F284930EE00422731 /* PresetHelper.swift */, ); path = Presets; sourceTree = ""; @@ -1216,6 +1220,7 @@ C41E871A2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */, C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */, C40C7F2827721FF600DDDCDC /* ActivePhpInstallation+Checks.swift in Sources */, + C463E380284930EE00422731 /* PresetHelper.swift in Sources */, C4EE55A927708B9E001DF387 /* PMHeaderView.swift in Sources */, C41C02A927E61A65009F26CB /* ValetSite+Fake.swift in Sources */, C4C0E8DF27F88AEB002D32A9 /* FakeSiteScanner.swift in Sources */, @@ -1347,6 +1352,7 @@ C4C0E8E327F88B13002D32A9 /* ValetSiteScanner.swift in Sources */, C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */, C4B5635F276AB09000F12CCB /* VersionExtractor.swift in Sources */, + C463E381284930EE00422731 /* PresetHelper.swift in Sources */, C46FA98C2822F08F00D78807 /* PhpConfigurationTest.swift in Sources */, C4BF90C127C57C220054E78C /* MainMenu+FixMyValet.swift in Sources */, C4C0E8EB27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */, diff --git a/README.md b/README.md index 98aaeca..5223b62 100644 --- a/README.md +++ b/README.md @@ -309,7 +309,7 @@ All of these apps should just be detected correctly, no matter their location on To see which files are checked to determine availability, see [this file](./phpmon/Domain/Helpers/Application.swift). -You can add your own apps by creating and editing a `~/.phpmon.conf.json` file, with the following entry: +You can add your own apps by creating and editing a `~/.config/phpmon/config.json` file, with the following entry:
 {
diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift
index 7ee6502..ad95113 100644
--- a/phpmon/Domain/Menu/MainMenu+Actions.swift
+++ b/phpmon/Domain/Menu/MainMenu+Actions.swift
@@ -145,6 +145,14 @@ extension MainMenu {
         }
     }
 
+    @objc func rollbackPreset() {
+        asyncExecution {
+            PresetHelper.rollbackPreset?.apply()
+            PresetHelper.rollbackPreset = nil
+            MainMenu.shared.rebuild()
+        }
+    }
+
     @objc func togglePreset(sender: PresetMenuItem) {
         asyncExecution {
             sender.preset?.apply()
diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift
index 43badf9..759ae3d 100644
--- a/phpmon/Domain/Menu/MainMenu+Startup.swift
+++ b/phpmon/Domain/Menu/MainMenu+Startup.swift
@@ -62,22 +62,10 @@ extension MainMenu {
         App.shared.handlePhpConfigWatcher()
 
         // Detect applications (preset + custom)
-        Log.info("Detecting applications...")
-        App.shared.detectedApplications = Application.detectPresetApplications()
+        self.loadApps()
 
-        let customApps = Preferences.custom.scanApps.map { appName in
-            return Application(appName, .user_supplied)
-        }.filter { app in
-            return app.isInstalled()
-        }
-
-        App.shared.detectedApplications.append(contentsOf: customApps)
-
-        let appNames = App.shared.detectedApplications.map { app in
-            return app.name
-        }
-
-        Log.info("Detected applications: \(appNames)")
+        // Load the rollback preset
+        PresetHelper.loadRollbackPresetFromFile()
 
         // Load the global hotkey
         App.shared.loadGlobalHotkey()
@@ -133,4 +121,23 @@ extension MainMenu {
             Task { await startup() }
         }
     }
+
+    private func loadApps() {
+        Log.info("Detecting applications...")
+        App.shared.detectedApplications = Application.detectPresetApplications()
+
+        let customApps = Preferences.custom.scanApps.map { appName in
+            return Application(appName, .user_supplied)
+        }.filter { app in
+            return app.isInstalled()
+        }
+
+        App.shared.detectedApplications.append(contentsOf: customApps)
+
+        let appNames = App.shared.detectedApplications.map { app in
+            return app.name
+        }
+
+        Log.info("Detected applications: \(appNames)")
+    }
 }
diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift
index 302181d..224d729 100644
--- a/phpmon/Domain/Menu/StatusMenu+Items.swift
+++ b/phpmon/Domain/Menu/StatusMenu+Items.swift
@@ -103,8 +103,11 @@ extension StatusMenu {
         presetsMenu.addItem(NSMenuItem.separator())
         presetsMenu.addItem(NSMenuItem(
             title: "Revert to Previous Configuration...",
-            action: #selector(MainMenu.restartDnsMasq), keyEquivalent: "")
-        )
+            action: PresetHelper.rollbackPreset != nil
+                ? #selector(MainMenu.rollbackPreset)
+                : nil,
+            keyEquivalent: ""
+        ))
         presetsMenu.addItem(NSMenuItem.separator())
         presetsMenu.addItem(NSMenuItem(
             title: "\(Preferences.custom.presets.count) profiles loaded from configuration file",
diff --git a/phpmon/Domain/Preferences/Preferences.swift b/phpmon/Domain/Preferences/Preferences.swift
index 196b062..e2b009d 100644
--- a/phpmon/Domain/Preferences/Preferences.swift
+++ b/phpmon/Domain/Preferences/Preferences.swift
@@ -173,12 +173,12 @@ class Preferences {
     // MARK: - Custom Preferences
 
     private func loadCustomPreferences() {
-        let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.phpmon.conf.json")
+        let url = URL(fileURLWithPath: "/Users/\(Paths.whoami)/.config/phpmon/config.json")
         if Filesystem.fileExists(url.path) {
-            Log.info("A custom .phpmon.conf.json file was found. Attempting to parse...")
+            Log.info("A custom ~/.config/phpmon/config.json file was found. Attempting to parse...")
             loadCustomPreferencesFile(url)
         } else {
-            Log.info("There was no .phpmon.conf.json file to be loaded.")
+            Log.info("There was no /.config/phpmon/config.json file to be loaded.")
         }
     }
 
@@ -189,10 +189,10 @@ class Preferences {
                 from: try! String(contentsOf: url, encoding: .utf8).data(using: .utf8)!
             )
 
-            Log.info("The .phpmon.conf.json file was successfully parsed.")
+            Log.info("The ~/.config/phpmon/config.json file was successfully parsed.")
             Log.info("There are \(customPreferences.presets.count) custom presets.")
         } catch {
-            Log.warn("The .phpmon.conf.json file seems to be missing or malformed.")
+            Log.warn("The ~/.config/phpmon/config.json file seems to be missing or malformed.")
         }
     }
 
diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift
index f89e665..5340e7b 100644
--- a/phpmon/Domain/Presets/Preset.swift
+++ b/phpmon/Domain/Presets/Preset.swift
@@ -51,6 +51,10 @@ struct Preset: Codable {
                 }
             }
 
+            // Reload what rollback file exists
+            PresetHelper.loadRollbackPresetFromFile()
+
+            // Restart PHP FPM process (also reloads menu, which will show the preset rollback)
             Actions.restartPhpFpm()
         }
     }
diff --git a/phpmon/Domain/Presets/PresetHelper.swift b/phpmon/Domain/Presets/PresetHelper.swift
new file mode 100644
index 0000000..86bc0ae
--- /dev/null
+++ b/phpmon/Domain/Presets/PresetHelper.swift
@@ -0,0 +1,37 @@
+//
+//  PresetHelper.swift
+//  PHP Monitor
+//
+//  Created by Nico Verbruggen on 02/06/2022.
+//  Copyright © 2022 Nico Verbruggen. All rights reserved.
+//
+
+import Foundation
+
+class PresetHelper {
+
+    static var rollbackPreset: Preset?
+
+    // MARK: - Reloading Configuration
+
+    public static func loadRollbackPresetFromFile() {
+        guard let revert = try? String(
+            contentsOfFile: "/Users/\(Paths.whoami)/.config/phpmon/preset_revert.json",
+            encoding: .utf8
+        ) else {
+            PresetHelper.rollbackPreset = nil
+            return
+        }
+
+        guard let preset = try? JSONDecoder().decode(
+            Preset.self,
+            from: revert.data(using: .utf8)!
+        ) else {
+            PresetHelper.rollbackPreset = nil
+            return
+        }
+
+        PresetHelper.rollbackPreset = preset
+    }
+
+}

From 01288154a7a87f6ba8363bc2282eec06d95b24ed Mon Sep 17 00:00:00 2001
From: Nico Verbruggen 
Date: Fri, 3 Jun 2022 22:57:22 +0200
Subject: [PATCH 24/58] =?UTF-8?q?=F0=9F=91=8C=20Better=20alerts=20(WIP)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 phpmon/Domain/App/InterAppHandler.swift |  8 +++---
 phpmon/Domain/Presets/Preset.swift      | 33 ++++++++++++++-----------
 phpmon/Localizable.strings              | 12 +++++++++
 3 files changed, 35 insertions(+), 18 deletions(-)

diff --git a/phpmon/Domain/App/InterAppHandler.swift b/phpmon/Domain/App/InterAppHandler.swift
index 56b17af..2529441 100644
--- a/phpmon/Domain/App/InterAppHandler.swift
+++ b/phpmon/Domain/App/InterAppHandler.swift
@@ -57,9 +57,11 @@ class InterApp {
                 MainMenu.shared.switchToPhpVersion(version)
             } else {
                 BetterAlert().withInformation(
-                    title: "Unsupported version",
-                    subtitle: "PHP Monitor can't switch to PHP \(version), as it may not be installed or available."
-                ).withPrimary(text: "OK").show()
+                    title: "alert.php_switch_unavailable.title".localized,
+                    subtitle: "alert.php_switch_unavailable.subtitle".localized(version)
+                ).withPrimary(
+                    text: "alert.php_switch_unavailable.ok".localized
+                ).show()
             }
         })
     ]}
diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift
index 5340e7b..8fad54e 100644
--- a/phpmon/Domain/Presets/Preset.swift
+++ b/phpmon/Domain/Presets/Preset.swift
@@ -33,7 +33,11 @@ struct Preset: Codable {
 
             // Apply the PHP version if is considered a valid version
             if self.version != nil {
-                await switchToPhpVersionIfValid()
+                if await !switchToPhpVersionIfValid() {
+                    PresetHelper.rollbackPreset = nil
+                    Actions.restartPhpFpm()
+                    return
+                }
             }
 
             // Apply the configuration changes first
@@ -61,30 +65,29 @@ struct Preset: Codable {
 
     // MARK: - Apply Functionality
 
-    private func switchToPhpVersionIfValid() async {
+    private func switchToPhpVersionIfValid() async -> Bool {
         if PhpEnv.shared.currentInstall.version.short == self.version! {
             Log.info("The version we are supposed to switch to is already active.")
-            return
+            return true
         }
 
         if PhpEnv.shared.availablePhpVersions.first(where: { $0 == self.version }) != nil {
             await MainMenu.shared.switchToPhp(self.version!)
-            return
+            return true
         } else {
             DispatchQueue.main.async {
-                BetterAlert()
-                    .withInformation(
-                        title: "PHP version unavailable",
-                        subtitle: "You have specified a PHP version (\(version!)) that is unavailable.",
-                        description: "Please make sure this version of PHP is installed "
-                        + "and you can switch to it in the dropdown. "
-                        + "Currently supported versions include: "
-                        + "\(PhpEnv.shared.availablePhpVersions.joined(separator: ", "))."
+                BetterAlert().withInformation(
+                    title: "alert.php_switch_unavailable.title".localized,
+                    subtitle: "alert.php_switch_unavailable.subtitle".localized(version!),
+                    description: "alert.php_switch_unavailable.info".localized(
+                        version!,
+                        PhpEnv.shared.availablePhpVersions.joined(separator: ", ")
                     )
-                    .withPrimary(text: "OK")
-                    .show()
+                ).withPrimary(
+                    text: "alert.php_switch_unavailable.ok".localized
+                ).show()
             }
-            return
+            return false
         }
     }
 
diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings
index f16d0cf..afd6f15 100644
--- a/phpmon/Localizable.strings
+++ b/phpmon/Localizable.strings
@@ -323,6 +323,18 @@ For optimal support of the latest versions of PHP and proper version switching,
 
 You can do this by running `composer global update` in your terminal. After that, run `valet install` again. For best results, restart PHP Monitor after that.";
 
+// PHP version unavailable
+"alert.php_switch_unavailable.title" = "Unsupported PHP version";
+"alert.php_switch_unavailable.subtitle" = "PHP Monitor can't switch to PHP %@, as it may not be installed or available. Applying this preset has been cancelled.";
+"alert.php_switch_unavailable.info" = "Please make sure PHP %@ is installed and you can switch to it in the dropdown. Currently supported versions include PHP: %@.";
+"alert.php_switch_unavailable.ok" = "OK";
+
+// Revert
+"alert.revert_description.title" = "Revert Configuration?";
+"alert.revert_description.subtitle" = "PHP Monitor can revert to the previous configuration that was active. Here's what will be applied: \n\n%@";
+"alert.revert_description.ok" = "Revert";
+"alert.revert_description.cancel" = "Cancel";
+
 // STARTUP
 
 /// 0. Architecture mismatch

From 94df551b4b7a7f8b7258379903559d7b6c875466 Mon Sep 17 00:00:00 2001
From: Nico Verbruggen 
Date: Fri, 3 Jun 2022 23:52:40 +0200
Subject: [PATCH 25/58] =?UTF-8?q?=F0=9F=91=8C=20Show=20alert=20with=20what?=
 =?UTF-8?q?=20will=20be=20rolled=20back?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 phpmon/Domain/Menu/MainMenu+Actions.swift | 20 ++++++++++++++++-
 phpmon/Domain/Presets/Preset.swift        | 26 +++++++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift
index ad95113..3a8194c 100644
--- a/phpmon/Domain/Menu/MainMenu+Actions.swift
+++ b/phpmon/Domain/Menu/MainMenu+Actions.swift
@@ -145,7 +145,7 @@ extension MainMenu {
         }
     }
 
-    @objc func rollbackPreset() {
+    private func performRollback() {
         asyncExecution {
             PresetHelper.rollbackPreset?.apply()
             PresetHelper.rollbackPreset = nil
@@ -153,6 +153,24 @@ extension MainMenu {
         }
     }
 
+    @objc func rollbackPreset() {
+        guard let preset = PresetHelper.rollbackPreset else {
+            return
+        }
+
+        BetterAlert().withInformation(
+            title: "alert.revert_description.title".localized,
+            subtitle: "alert.revert_description.subtitle".localized(
+                preset.textDescription
+            )
+        )
+        .withPrimary(text: "alert.revert_description.ok".localized, action: { _ in
+            self.performRollback()
+        })
+        .withSecondary(text: "alert.revert_description.cancel".localized)
+        .show()
+    }
+
     @objc func togglePreset(sender: PresetMenuItem) {
         asyncExecution {
             sender.preset?.apply()
diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift
index 8fad54e..a92629f 100644
--- a/phpmon/Domain/Presets/Preset.swift
+++ b/phpmon/Domain/Presets/Preset.swift
@@ -21,6 +21,32 @@ struct Preset: Codable {
              configuration = "configuration"
     }
 
+    var textDescription: String {
+        var text = ""
+
+        if self.version != nil {
+            text += "Switches to PHP \(self.version!).\n"
+        }
+
+        if !self.extensions.isEmpty {
+            text += "\nApplies the following extensions:\n"
+        }
+
+        for (ext, extValue) in self.extensions {
+            text += "• \(ext): \(extValue ? "enabled" : "disabled") \n"
+        }
+
+        if !self.configuration.isEmpty {
+            text += "\nApplies the following configuration values:\n"
+        }
+
+        for (key, value) in self.configuration {
+            text += "• \(key)=\(value ?? "(empty)") \n"
+        }
+
+        return text
+    }
+
     // MARK: Applying
 
     /**

From fbf21584889497cd619680636f8dbbe6136cac03 Mon Sep 17 00:00:00 2001
From: Nico Verbruggen 
Date: Sun, 5 Jun 2022 03:03:00 +0200
Subject: [PATCH 26/58] =?UTF-8?q?=F0=9F=91=8C=20Improved=20alerts,=20local?=
 =?UTF-8?q?ization?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../Common/PHP/Homebrew/HomebrewService.swift |  4 ++-
 phpmon/Domain/Menu/MainMenu+Actions.swift     |  3 ++-
 phpmon/Domain/Presets/Preset.swift            | 25 +++++++++++++++----
 phpmon/Localizable.strings                    |  8 ++++++
 4 files changed, 33 insertions(+), 7 deletions(-)

diff --git a/phpmon/Common/PHP/Homebrew/HomebrewService.swift b/phpmon/Common/PHP/Homebrew/HomebrewService.swift
index a2cfbf6..7191016 100644
--- a/phpmon/Common/PHP/Homebrew/HomebrewService.swift
+++ b/phpmon/Common/PHP/Homebrew/HomebrewService.swift
@@ -32,7 +32,9 @@ struct HomebrewService: Decodable, Equatable {
                 .decode([HomebrewService].self, from: data)
                 .filter({ return filter.contains($0.name) })
 
-            completion(services)
+            DispatchQueue.main.async {
+                completion(services)
+            }
         }
     }
 }
diff --git a/phpmon/Domain/Menu/MainMenu+Actions.swift b/phpmon/Domain/Menu/MainMenu+Actions.swift
index 3a8194c..071cd57 100644
--- a/phpmon/Domain/Menu/MainMenu+Actions.swift
+++ b/phpmon/Domain/Menu/MainMenu+Actions.swift
@@ -164,8 +164,9 @@ extension MainMenu {
                 preset.textDescription
             )
         )
-        .withPrimary(text: "alert.revert_description.ok".localized, action: { _ in
+        .withPrimary(text: "alert.revert_description.ok".localized, action: { alert in
             self.performRollback()
+            alert.close(with: .OK)
         })
         .withSecondary(text: "alert.revert_description.cancel".localized)
         .show()
diff --git a/phpmon/Domain/Presets/Preset.swift b/phpmon/Domain/Presets/Preset.swift
index a92629f..340aa09 100644
--- a/phpmon/Domain/Presets/Preset.swift
+++ b/phpmon/Domain/Presets/Preset.swift
@@ -21,27 +21,42 @@ struct Preset: Codable {
              configuration = "configuration"
     }
 
+    /**
+     What the preset does, in text form. Used to display what's going on.
+     */
     var textDescription: String {
         var text = ""
 
         if self.version != nil {
-            text += "Switches to PHP \(self.version!).\n"
+            text += "alert.preset_description.switcher_version".localized(self.version!)
         }
 
         if !self.extensions.isEmpty {
-            text += "\nApplies the following extensions:\n"
+            // Show a subsection header
+            text += "alert.preset_description.applying_extensions".localized
         }
 
         for (ext, extValue) in self.extensions {
-            text += "• \(ext): \(extValue ? "enabled" : "disabled") \n"
+            // An extension is either enabled or disabled
+            let status = extValue
+                ? "alert.preset_description.enabled".localized
+                : "alert.preset_description.disabled".localized
+            text += "• \(ext): \(status)\n"
         }
 
         if !self.configuration.isEmpty {
-            text += "\nApplies the following configuration values:\n"
+            // Extra spacing if the previous section was extensions
+            if !self.extensions.isEmpty {
+                text += "\n"
+            }
+
+            // Show a subsection header
+            text += "alert.preset_description.applying_config".localized
         }
 
         for (key, value) in self.configuration {
-            text += "• \(key)=\(value ?? "(empty)") \n"
+            // A value is either displayed, or the value is "(empty)"
+            text += "• \(key)=\(value ?? "alert.preset_description.empty".localized) \n"
         }
 
         return text
diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings
index afd6f15..630389c 100644
--- a/phpmon/Localizable.strings
+++ b/phpmon/Localizable.strings
@@ -323,6 +323,14 @@ For optimal support of the latest versions of PHP and proper version switching,
 
 You can do this by running `composer global update` in your terminal. After that, run `valet install` again. For best results, restart PHP Monitor after that.";
 
+// Preset text description
+"alert.preset_description.switcher_version" = "Switches to PHP %@.\n\n";
+"alert.preset_description.applying_extensions" = "Applies the following extensions:\n";
+"alert.preset_description.applying_config" = "Applies the following configuration values:\n";
+"alert.preset_description.enabled" = "enabled";
+"alert.preset_description.disabled" = "disabled";
+"alert.preset_description.empty" = "(empty)";
+
 // PHP version unavailable
 "alert.php_switch_unavailable.title" = "Unsupported PHP version";
 "alert.php_switch_unavailable.subtitle" = "PHP Monitor can't switch to PHP %@, as it may not be installed or available. Applying this preset has been cancelled.";

From 998bbd231a4fd1d5e74fd3acd44fb7d9d3fbc303 Mon Sep 17 00:00:00 2001
From: Nico Verbruggen 
Date: Sun, 5 Jun 2022 12:52:59 +0200
Subject: [PATCH 27/58] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20preferenc?=
 =?UTF-8?q?es=20window?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 PHP Monitor.xcodeproj/project.pbxproj         | 12 +--
 phpmon/Domain/App/Base.lproj/Main.storyboard  | 40 +++++++---
 phpmon/Domain/Menu/MainMenu.swift             |  2 +-
 ...efsVC.swift => GeneralPreferencesVC.swift} | 80 +++++++++----------
 phpmon/Domain/Preferences/PrefsWC.swift       | 63 +++++++++++++++
 .../Views/HotkeyPreferenceView.swift          |  5 +-
 6 files changed, 140 insertions(+), 62 deletions(-)
 rename phpmon/Domain/Preferences/{PrefsVC.swift => GeneralPreferencesVC.swift} (68%)

diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj
index eff0c4f..f38c0fb 100644
--- a/PHP Monitor.xcodeproj/project.pbxproj	
+++ b/PHP Monitor.xcodeproj/project.pbxproj	
@@ -7,7 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
-		5420395926135DC100FB00FA /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
+		5420395926135DC100FB00FA /* GeneralPreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* GeneralPreferencesVC.swift */; };
 		5420395F2613607600FB00FA /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
 		5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; };
 		5489625928313231004F647A /* CreatedFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5489625728312FAD004F647A /* CreatedFromFile.swift */; };
@@ -143,7 +143,7 @@
 		C476FF9822B0DD830098105B /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; };
 		C4811D2422D70A4700B5F6B3 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2322D70A4700B5F6B3 /* App.swift */; };
 		C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4811D2922D70F9A00B5F6B3 /* MainMenu.swift */; };
-		C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* PrefsVC.swift */; };
+		C481F79726164A78004FBCFF /* GeneralPreferencesVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395826135DC100FB00FA /* GeneralPreferencesVC.swift */; };
 		C481F79A26164A7C004FBCFF /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5420395E2613607600FB00FA /* Preferences.swift */; };
 		C484437B2804BB560041A78A /* ValetProxyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C484437A2804BB560041A78A /* ValetProxyScanner.swift */; };
 		C484437C2804BB560041A78A /* ValetProxyScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C484437A2804BB560041A78A /* ValetProxyScanner.swift */; };
@@ -288,7 +288,7 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
-		5420395826135DC100FB00FA /* PrefsVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsVC.swift; sourceTree = ""; };
+		5420395826135DC100FB00FA /* GeneralPreferencesVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesVC.swift; sourceTree = ""; };
 		5420395E2613607600FB00FA /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; };
 		5489625728312FAD004F647A /* CreatedFromFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatedFromFile.swift; sourceTree = ""; };
 		54A18D3F282A566E000A0D81 /* nginx-secure-proxy-custom-tld.test */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "nginx-secure-proxy-custom-tld.test"; sourceTree = ""; };
@@ -474,7 +474,7 @@
 			isa = PBXGroup;
 			children = (
 				C4998F092617633900B2526E /* PrefsWC.swift */,
-				5420395826135DC100FB00FA /* PrefsVC.swift */,
+				5420395826135DC100FB00FA /* GeneralPreferencesVC.swift */,
 				5420395E2613607600FB00FA /* Preferences.swift */,
 				C4C3ED4227834C5200AB15D8 /* CustomPrefs.swift */,
 				C4068CA927B0890D00544CD5 /* MenuBarIcons.swift */,
@@ -1202,7 +1202,7 @@
 				C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */,
 				C4AF9F7A2754499000D44ED0 /* Valet.swift in Sources */,
 				C4C0E8EA27F88B80002D32A9 /* ValetProxy+Fake.swift in Sources */,
-				5420395926135DC100FB00FA /* PrefsVC.swift in Sources */,
+				5420395926135DC100FB00FA /* GeneralPreferencesVC.swift in Sources */,
 				C43603A0275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */,
 				5489625828312FAD004F647A /* CreatedFromFile.swift in Sources */,
 				C4068CA727B07A1300544CD5 /* SelectPreferenceView.swift in Sources */,
@@ -1368,7 +1368,7 @@
 				C4D936CB27E3EE4A00BD69FE /* DomainListCellProtocol.swift in Sources */,
 				C4B97B76275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */,
 				C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */,
-				C481F79726164A78004FBCFF /* PrefsVC.swift in Sources */,
+				C481F79726164A78004FBCFF /* GeneralPreferencesVC.swift in Sources */,
 				C41E871B2763D42300161EE0 /* DomainListVC+ContextMenu.swift in Sources */,
 				C40C7F3127722E8D00DDDCDC /* Logger.swift in Sources */,
 				C4068CAB27B0890D00544CD5 /* MenuBarIcons.swift in Sources */,
diff --git a/phpmon/Domain/App/Base.lproj/Main.storyboard b/phpmon/Domain/App/Base.lproj/Main.storyboard
index d627efb..fd246f2 100644
--- a/phpmon/Domain/App/Base.lproj/Main.storyboard
+++ b/phpmon/Domain/App/Base.lproj/Main.storyboard
@@ -324,14 +324,15 @@
         
         
             
+                
                 
                     
                         
                         
-                        
+                        
                         
                         
-                            
+                            
                             
                         
                         
@@ -343,17 +344,16 @@
                         
                     
                     
-                        
+                        
                     
                 
-                
             
             
         
         
         
             
-                
+                
                     
                         
                         
@@ -378,7 +378,27 @@
                 
                 
             
-            
+            
+        
+        
+        
+            
+                
+                
+                    
+                        
+                        
+                        
+                        
+                            
+                        
+                    
+                    
+                        
+                    
+                
+            
+            
         
         
         
@@ -532,7 +552,7 @@ Gw