From 493b5945f9f542ad4e83a8c2a2dba0f95bf1f4d9 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Sun, 28 Nov 2021 02:20:56 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=8C=20Major=20changes=20to=20PHP=20ver?= =?UTF-8?q?sion=20detection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * The information extracted from Homebrew's JSON command now also includes information about linked keg and installations. * The mapped versions in the App class now contain information about the Homebrew installation as well. * A HomebrewDiagnostics class has been added, which is currently able to detect conflicts between the `php` formulae of core and the `shivammathur/php` tap (which is currently an issue, see #54) * Alerts are now displayed as critical if they are truly problematic. * PhpInstallation was renamed to ActivePhpInstallation, to make room for a generic PhpInstallation object which contains cached info. * Shell.pipe() now returns the contents of standardError if standardOutput was empty and there was some data in standardError. This makes it easier to debug the output of commands that output to standardError. (For example, failed brew commands might.) --- PHP Monitor.xcodeproj/project.pbxproj | 24 ++- phpmon-tests/BrewJsonParserTest.swift | 3 + phpmon/Domain/Core/Actions.swift | 14 +- phpmon/Domain/Core/App.swift | 8 +- phpmon/Domain/Core/HomebrewDiagnostics.swift | 70 +++++++ phpmon/Domain/Core/Startup.swift | 6 +- phpmon/Domain/Helpers/Alert.swift | 8 +- phpmon/Domain/Helpers/HomebrewPackage.swift | 11 +- phpmon/Domain/Menu/MainMenu.swift | 13 +- phpmon/Domain/Menu/StatusMenu.swift | 2 +- phpmon/Domain/PHP/ActivePhpInstallation.swift | 174 +++++++++++++++++ phpmon/Domain/PHP/PhpInstallation.swift | 176 ++---------------- phpmon/Domain/Terminal/Shell.swift | 18 +- phpmon/Localizable.strings | 4 + 14 files changed, 339 insertions(+), 192 deletions(-) create mode 100644 phpmon/Domain/Core/HomebrewDiagnostics.swift create mode 100644 phpmon/Domain/PHP/ActivePhpInstallation.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 8d53e653..d2a42591 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -18,7 +18,7 @@ C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; }; C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; }; C41C1B4922B00A9800E7CF16 /* MenuBarImageGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */; }; - C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; }; + C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4C22B0215A00E7CF16 /* Actions.swift */; }; C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */; }; C42295DD2358D02000E263B2 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; }; @@ -49,6 +49,10 @@ C4ACA38F25C754C100060C66 /* PhpExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4ACA38E25C754C100060C66 /* PhpExtension.swift */; }; C4D8016622B1584700C6DA1B /* Startup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4D8016522B1584700C6DA1B /* Startup.swift */; }; C4EE188422D3386B00E126E5 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4EE188322D3386B00E126E5 /* Constants.swift */; }; + C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; + C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */; }; + C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; + C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F2E4392752F7D00020E974 /* PhpInstallation.swift */; }; C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4622B009A400E7CF16 /* Shell.swift */; }; C4F7809C25D80344000DBC97 /* CommandTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F7809B25D80344000DBC97 /* CommandTest.swift */; }; C4F7809F25D8037C000DBC97 /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = C42295DC2358D02000E263B2 /* Command.swift */; }; @@ -70,7 +74,7 @@ C4F780C925D80B75000DBC97 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C412E5FB25700D5300A1FB67 /* HomebrewPackage.swift */; }; C4F780CB25D80B75000DBC97 /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48D0CA225CC992000CC7490 /* StatsView.swift */; }; - C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */; }; + C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */; }; C4F780CD25D80B75000DBC97 /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = C476FF9722B0DD830098105B /* Alert.swift */; }; C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = C474B00524C0E98C00066A22 /* LocalNotification.swift */; }; C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; }; @@ -101,7 +105,7 @@ C41C1B4022B0098000E7CF16 /* phpmon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = phpmon.entitlements; sourceTree = ""; }; C41C1B4622B009A400E7CF16 /* Shell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shell.swift; sourceTree = ""; }; C41C1B4822B00A9800E7CF16 /* MenuBarImageGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarImageGenerator.swift; sourceTree = ""; }; - C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = ""; }; + C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivePhpInstallation.swift; sourceTree = ""; }; C41C1B4C22B0215A00E7CF16 /* Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Actions.swift; sourceTree = ""; }; C41CD0282628D8EE0065BBED /* GlobalKeybindPreference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalKeybindPreference.swift; sourceTree = ""; }; C42295DC2358D02000E263B2 /* Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Command.swift; sourceTree = ""; }; @@ -129,6 +133,8 @@ C4E713562570150F00007428 /* SECURITY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = SECURITY.md; sourceTree = ""; }; C4E713572570151400007428 /* docs */ = {isa = PBXFileReference; lastKnownFileType = folder; path = docs; sourceTree = ""; }; C4EE188322D3386B00E126E5 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomebrewDiagnostics.swift; sourceTree = ""; }; + C4F2E4392752F7D00020E974 /* PhpInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpInstallation.swift; sourceTree = ""; }; C4F7807425D7F7E5000DBC97 /* RELEASE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = RELEASE.md; 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 = ""; }; @@ -173,7 +179,8 @@ 54B20EDF263AA22C00D3250E /* PHP */ = { isa = PBXGroup; children = ( - C41C1B4A22B019FF00E7CF16 /* PhpInstallation.swift */, + C4F2E4392752F7D00020E974 /* PhpInstallation.swift */, + C41C1B4A22B019FF00E7CF16 /* ActivePhpInstallation.swift */, C4ACA38E25C754C100060C66 /* PhpExtension.swift */, ); path = PHP; @@ -279,6 +286,7 @@ C4811D2322D70A4700B5F6B3 /* App.swift */, C4D8016522B1584700C6DA1B /* Startup.swift */, C41C1B4C22B0215A00E7CF16 /* Actions.swift */, + C4F2E4362752F0870020E974 /* HomebrewDiagnostics.swift */, ); path = Core; sourceTree = ""; @@ -439,8 +447,10 @@ C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */, 5420395926135DC100FB00FA /* PrefsVC.swift in Sources */, C41C1B4722B009A400E7CF16 /* Shell.swift in Sources */, + C4F2E43A2752F7D00020E974 /* PhpInstallation.swift in Sources */, C41C1B4D22B0215A00E7CF16 /* Actions.swift in Sources */, C48D0CA325CC992000CC7490 /* StatsView.swift in Sources */, + C4F2E4372752F0870020E974 /* HomebrewDiagnostics.swift in Sources */, C41CD0292628D8EE0065BBED /* GlobalKeybindPreference.swift in Sources */, C42295DD2358D02000E263B2 /* Command.swift in Sources */, C4811D2422D70A4700B5F6B3 /* App.swift in Sources */, @@ -451,7 +461,7 @@ C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */, C42759672627662800093CAE /* NSMenuExtension.swift in Sources */, - C41C1B4B22B019FF00E7CF16 /* PhpInstallation.swift in Sources */, + C41C1B4B22B019FF00E7CF16 /* ActivePhpInstallation.swift in Sources */, C49EAB46259FC305007F6C3B /* Paths.swift in Sources */, C476FF9822B0DD830098105B /* Alert.swift in Sources */, C474B00624C0E98C00066A22 /* LocalNotification.swift in Sources */, @@ -469,13 +479,14 @@ 54EAC806262F212B0092D14E /* GlobalKeybindPreference.swift in Sources */, C4F780C425D80B75000DBC97 /* MainMenu.swift in Sources */, C4F780C825D80B75000DBC97 /* DateExtension.swift in Sources */, - C4F780CC25D80B75000DBC97 /* PhpInstallation.swift in Sources */, + C4F780CC25D80B75000DBC97 /* ActivePhpInstallation.swift in Sources */, C4F780B125D80B4D000DBC97 /* PhpExtension.swift in Sources */, C4F780CE25D80B75000DBC97 /* LocalNotification.swift in Sources */, C4FBFC532616485F00CDB8E1 /* PhpVersionDetectionTest.swift in Sources */, C43A8A2425D9D20D00591B77 /* BrewJsonParserTest.swift in Sources */, C4F780CA25D80B75000DBC97 /* HomebrewPackage.swift in Sources */, C4F780C025D80B6E000DBC97 /* Startup.swift in Sources */, + C4F2E4382752F08D0020E974 /* HomebrewDiagnostics.swift in Sources */, C4F780AE25D80B37000DBC97 /* ExtensionParserTest.swift in Sources */, C4F780C725D80B75000DBC97 /* StatusMenu.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, @@ -485,6 +496,7 @@ C4F780BA25D80B62000DBC97 /* AppDelegate.swift in Sources */, C4998F0B2617633900B2526E /* PrefsWC.swift in Sources */, C4F780A225D804AA000DBC97 /* Paths.swift in Sources */, + C4F2E43B27530F750020E974 /* PhpInstallation.swift in Sources */, C4F780BD25D80B65000DBC97 /* Constants.swift in Sources */, C4F780C325D80B75000DBC97 /* HeaderView.swift in Sources */, C4F7809625D7FBF8000DBC97 /* Shell.swift in Sources */, diff --git a/phpmon-tests/BrewJsonParserTest.swift b/phpmon-tests/BrewJsonParserTest.swift index 1d2f5167..a91cbeef 100644 --- a/phpmon-tests/BrewJsonParserTest.swift +++ b/phpmon-tests/BrewJsonParserTest.swift @@ -23,6 +23,9 @@ class BrewJsonParserTest: XCTestCase { XCTAssertEqual(package.name, "php") XCTAssertEqual(package.full_name, "php") XCTAssertEqual(package.aliases.first!, "php@8.0") + XCTAssertEqual(package.installed.contains(where: { installed in + installed.version.starts(with: "8.0") + }), true) } } diff --git a/phpmon/Domain/Core/Actions.swift b/phpmon/Domain/Core/Actions.swift index a75537f9..00cc313a 100644 --- a/phpmon/Domain/Core/Actions.swift +++ b/phpmon/Domain/Core/Actions.swift @@ -41,20 +41,12 @@ class Actions { */ public static func extractPhpLongVersions() { - var mappedVersions: [String: String] = [:] + var mappedVersions: [String: PhpInstallation] = [:] App.shared.availablePhpVersions.forEach { version in - let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" - var longVersion = version - if Shell.fileExists(phpConfigExecutablePath) { - longVersion = Command.execute( - path: phpConfigExecutablePath, - arguments: ["--version"] - ) - } - mappedVersions[version] = longVersion + mappedVersions[version] = PhpInstallation(version) } - App.shared.cachedPhpVersionNumbers = mappedVersions + App.shared.cachedPhpInstallations = mappedVersions } /** diff --git a/phpmon/Domain/Core/App.swift b/phpmon/Domain/Core/App.swift index e59b351c..49069e79 100644 --- a/phpmon/Domain/Core/App.swift +++ b/phpmon/Domain/Core/App.swift @@ -17,7 +17,7 @@ class App { } /** Information about the currently linked PHP installation. */ - static var phpInstall: PhpInstallation? { + static var phpInstall: ActivePhpInstallation? { return App.shared.currentInstall } @@ -42,7 +42,7 @@ class App { /** The currently active installation of PHP. */ - var currentInstall: PhpInstallation? = nil + var currentInstall: ActivePhpInstallation? = nil /** All available versions of PHP. @@ -52,7 +52,7 @@ class App { /** Cached information about the PHP installations; contains only the full version number at this point. */ - var cachedPhpVersionNumbers : [String: String] = [:] + var cachedPhpInstallations : [String: PhpInstallation] = [:] /** The timer that will periodically fetch the PHP version that is currently active. @@ -62,7 +62,7 @@ class App { /** Information we were able to discern from the Homebrew info command (as JSON). */ - var brewPhpPackage: HomebrewPackage? = nil { + var brewPhpPackage: HomebrewPackage! = nil { didSet { brewPhpVersion = brewPhpPackage!.version } diff --git a/phpmon/Domain/Core/HomebrewDiagnostics.swift b/phpmon/Domain/Core/HomebrewDiagnostics.swift new file mode 100644 index 00000000..f762180e --- /dev/null +++ b/phpmon/Domain/Core/HomebrewDiagnostics.swift @@ -0,0 +1,70 @@ +// +// AliasConflict.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 28/11/2021. +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class HomebrewDiagnostics { + + enum Errors: String { + case aliasConflict = "alias_conflict" + case installationMismatch = "installation_mismatch" + } + + static let shared = HomebrewDiagnostics() + var errors: [HomebrewDiagnostics.Errors] = [] + + init() { + if self.determineAliasConflicts() { + self.errors.append(.aliasConflict) + } + } + + /** + It is possible to have the `shivammathur/php` tap installed, and for the core homebrew information to be outdated. + This will then result in two different aliases claiming to point to the same formula (`php`). + This will break all linking functionality in PHP Monitor, and the user needs to be informed of this. + + This check only needs to be performed if the `shivammathur/php` tap is active. + */ + public func determineAliasConflicts() -> Bool + { + let tapAlias = Shell.pipe("\(Paths.brew) info shivammathur/php/php --json") + + if tapAlias.contains("brew tap shivammathur/php") || tapAlias.contains("Error") { + print("The user does not appear to have tapped: shivammathur/php") + return false + } else { + print("The user DOES have the following tapped: shivammathur/php") + print("Checking for `php` formula conflicts...") + + let tapPhp = try! JSONDecoder().decode( + [HomebrewPackage].self, + from: tapAlias.data(using: .utf8)! + ).first! + + if tapPhp.version != App.shared.brewPhpVersion { + print("The `php` formula alias seems to be the different between the tap and core. This could be a problem!") + print("Determining whether both of these versions are installed...") + + let bothInstalled = App.shared.availablePhpVersions.contains(tapPhp.version) + && App.shared.availablePhpVersions.contains(App.shared.brewPhpVersion) + + if bothInstalled { + print("Both conflicting aliases seem to be installed, warning the user!") + } else { + print("Conflicting aliases are not both installed, seems fine!") + } + + return bothInstalled + } + + print("All seems to be OK. No conflicts.") + return false + } + } +} diff --git a/phpmon/Domain/Core/Startup.swift b/phpmon/Domain/Core/Startup.swift index 25f0d8cf..42ebfa71 100644 --- a/phpmon/Domain/Core/Startup.swift +++ b/phpmon/Domain/Core/Startup.swift @@ -119,7 +119,11 @@ class Startup { DispatchQueue.main.async { [self] in // Present the information to the user - Alert.notify(message: messageText, info: informativeText) + Alert.notify( + message: messageText, + info: informativeText, + style: breaking ? .critical : .warning + ) // Only breaking issues will throw the extra retry modal breaking ? failureCallback() : () } diff --git a/phpmon/Domain/Helpers/Alert.swift b/phpmon/Domain/Helpers/Alert.swift index 78433cf2..735abad7 100644 --- a/phpmon/Domain/Helpers/Alert.swift +++ b/phpmon/Domain/Helpers/Alert.swift @@ -13,9 +13,11 @@ class Alert { messageText: String, informativeText: String, buttonTitle: String = "OK", - secondButtonTitle: String = "" + secondButtonTitle: String = "", + style: NSAlert.Style = .informational ) -> Bool { let alert = NSAlert.init() + alert.alertStyle = style alert.messageText = messageText alert.informativeText = informativeText alert.addButton(withTitle: buttonTitle) @@ -25,8 +27,8 @@ class Alert { return alert.runModal() == .alertFirstButtonReturn } - public static func notify(message: String, info: String) { - _ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "") + public static func notify(message: String, info: String, style: NSAlert.Style = .informational) { + _ = self.present(messageText: message, informativeText: info, buttonTitle: "OK", secondButtonTitle: "", style: style) } } diff --git a/phpmon/Domain/Helpers/HomebrewPackage.swift b/phpmon/Domain/Helpers/HomebrewPackage.swift index ffd8ea30..29e5c294 100644 --- a/phpmon/Domain/Helpers/HomebrewPackage.swift +++ b/phpmon/Domain/Helpers/HomebrewPackage.swift @@ -7,14 +7,23 @@ import Foundation -struct HomebrewPackage : Decodable { +struct HomebrewPackage: Decodable { let name: String let full_name: String let aliases: [String] + let installed: [HomebrewInstalled] + let linked_keg: String public var version: String { return aliases.first!.replacingOccurrences(of: "php@", with: "") } } + +struct HomebrewInstalled: Decodable { + let version: String + let built_as_bottle: Bool + let installed_as_dependency: Bool + let installed_on_request: Bool +} diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 49714bdd..e73f86b2 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -42,6 +42,17 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { */ private func onEnvironmentPass() { _ = Actions.detectPhpVersions() + + if HomebrewDiagnostics.shared.errors.contains(.aliasConflict) { + DispatchQueue.main.async { + Alert.notify( + message: "alert.php_alias_conflict.title".localized, + info: "alert.php_alias_conflict.info".localized, + style: .critical + ) + } + } + updatePhpVersionInStatusBar() let installation = App.phpInstall! @@ -165,7 +176,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { // MARK: - User Interface @objc func updatePhpVersionInStatusBar() { - App.shared.currentInstall = PhpInstallation() + App.shared.currentInstall = ActivePhpInstallation() refreshIcon() update() } diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index b3702280..f76fd251 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -45,7 +45,7 @@ class StatusMenu : NSMenu { // Get the short and long version let shortVersion = App.shared.availablePhpVersions[index] - let longVersion = App.shared.cachedPhpVersionNumbers[shortVersion]! + let longVersion = App.shared.cachedPhpInstallations[shortVersion]!.longVersion let long = Preferences.preferences[.fullPhpVersionDynamicIcon] as! Bool let versionString = long ? longVersion : shortVersion diff --git a/phpmon/Domain/PHP/ActivePhpInstallation.swift b/phpmon/Domain/PHP/ActivePhpInstallation.swift new file mode 100644 index 00000000..81dd40b9 --- /dev/null +++ b/phpmon/Domain/PHP/ActivePhpInstallation.swift @@ -0,0 +1,174 @@ +// +// ActivePhpInstallation.swift +// PHP Monitor +// +// Copyright © 2021 Nico Verbruggen. All rights reserved. +// + +import Foundation + +/** + An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory. + + When initialized, that version's .ini files are also scanned (for active or inactive extensions). + Integrity checks can be performed to determine whether PHP-FPM is configured correctly. + + - Note: Each installation has a separate version number. Using `version.short` is advisable if you want to interact with Homebrew. + */ +class ActivePhpInstallation { + + var version: Version! + var configuration: Configuration! + var extensions: [PhpExtension]! + + // MARK: - Computed + + var formula: String { + return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)" + } + + // MARK: - Initializer + + init() { + // Show information about the current version + self.getVersion() + + // If an error occurred, exit early + if (version.error) { + configuration = Configuration() + extensions = [] + return + } + + // Load extension information + let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") + extensions = PhpExtension.load(from: path) + + // Get configuration values + configuration = Configuration( + memory_limit: self.getByteCount(key: "memory_limit"), + upload_max_filesize: self.getByteCount(key: "upload_max_filesize"), + post_max_size: self.getByteCount(key: "post_max_size") + ) + + // Return a list of .ini files parsed after php.ini + let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"]) + .replacingOccurrences(of: "\n", with: "") + .split(separator: ",") + .map { String($0) } + + // See if any extensions are present in said .ini files + paths.forEach { (iniFilePath) in + let extensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath)) + if extensions.count > 0 { + self.extensions.append(contentsOf: extensions) + } + } + } + + /** + When the app tries to retrieve the version, the installation is considered broken if the output is nothing, + _or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case. + */ + private func getVersion() -> Void { + self.version = Version() + + let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true) + + if (version == "" || version.contains("Warning") || version.contains("Error")) { + self.version.short = "💩 BROKEN" + self.version.long = "" + self.version.error = true + return + } + + // That's the long version + self.version.long = version + + // Next up, let's strip away the minor version number + let segments = self.version.long.components(separatedBy: ".") + + // Get the first two elements + self.version.short = segments[0...1].joined(separator: ".") + } + + /** + Retrieves the display value for a specific key in the `.ini` file. + + The following values are valid: + * -1: unlimited (show the infinity icon) + * 10000: an integer = amount of bytes + * 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes + + If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here. + To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes ⚠️. + + - Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`. + */ + private func getByteCount(key: String) -> String { + let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"]) + + // Check if the value is unlimited + if (value == "-1") { + return "∞" + } + + // Check if the syntax is valid otherwise + let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) + let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first + return (match == nil) ? "⚠️" : "\(value)B" + } + + /** + It is always possible that the system configuration for PHP-FPM has not been set up for Valet. + This can occur when a user manually installs a new PHP version, but does not run `valet install`. + In that case, we should alert the user! + + - Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times. + This method actively presents a modal if said checks fails, so don't call this method too many times. + */ + public func notifyAboutBrokenPhpFpm() { + if !self.checkPhpFpmStatus() { + DispatchQueue.main.async { + Alert.notify( + message: "alert.php_fpm_broken.title".localized, + info: "alert.php_fpm_broken.info".localized, + style: .critical + ) + } + } + } + + /** + Determine if PHP-FPM is configured correctly. + + For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent + versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails, + that means that Valet won't work properly. + */ + private func checkPhpFpmStatus() -> Bool { + if self.version.short == "5.6" { + // The main PHP config file should contain `valet.sock` and then we're probably fine? + let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" + return Shell.pipe("cat \(fileName)").contains("valet.sock") + } + + // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) + return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf") + } + + // MARK: - Structs + + struct Version { + var short = "???" + var long = "???" + var error = false + } + + struct Configuration { + var memory_limit = "???" + var upload_max_filesize = "???" + var post_max_size = "???" + } + +} diff --git a/phpmon/Domain/PHP/PhpInstallation.swift b/phpmon/Domain/PHP/PhpInstallation.swift index e0eec3f4..11e513fb 100644 --- a/phpmon/Domain/PHP/PhpInstallation.swift +++ b/phpmon/Domain/PHP/PhpInstallation.swift @@ -1,173 +1,33 @@ // -// PhpInstallation.swift +// BrewPhpInstallation.swift // PHP Monitor // +// Created by Nico Verbruggen on 28/11/2021. // Copyright © 2021 Nico Verbruggen. All rights reserved. // import Foundation -/** - An installed version of PHP, that was detected by scanning the `/opt/php@version/bin` directory. - - When initialized, that version's .ini files are also scanned (for active or inactive extensions). - Integrity checks can be performed to determine whether PHP-FPM is configured correctly. - - - Note: Each installation has a separate version number. Using `version.short` is advisable if you want to interact with Homebrew. - */ class PhpInstallation { - - var version: Version! - var configuration: Configuration! - var extensions: [PhpExtension]! - - // MARK: - Computed - - var formula: String { - return (version.short == App.shared.brewPhpVersion) ? "php" : "php@\(version.short)" - } - - // MARK: - Initializer - - init() { - // Show information about the current version - self.getVersion() - - // If an error occurred, exit early - if (version.error) { - configuration = Configuration() - extensions = [] - return - } - - // Load extension information - let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") - extensions = PhpExtension.load(from: path) - - // Get configuration values - configuration = Configuration( - memory_limit: self.getByteCount(key: "memory_limit"), - upload_max_filesize: self.getByteCount(key: "upload_max_filesize"), - post_max_size: self.getByteCount(key: "post_max_size") - ) - - // Return a list of .ini files parsed after php.ini - let paths = Command.execute(path: Paths.php, arguments: ["-r", "echo php_ini_scanned_files();"]) - .replacingOccurrences(of: "\n", with: "") - .split(separator: ",") - .map { String($0) } - - // See if any extensions are present in said .ini files - paths.forEach { (iniFilePath) in - let extensions = PhpExtension.load(from: URL(fileURLWithPath: iniFilePath)) - if extensions.count > 0 { - self.extensions.append(contentsOf: extensions) - } - } - } - /** - When the app tries to retrieve the version, the installation is considered broken if the output is nothing, - _or_ if the output contains the word "Warning" or "Error". In normal situations this should not be the case. - */ - private func getVersion() -> Void { - self.version = Version() - - let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true) - - if (version == "" || version.contains("Warning") || version.contains("Error")) { - self.version.short = "💩 BROKEN" - self.version.long = "" - self.version.error = true - return + var longVersion: String + var homebrewInfo: HomebrewPackage + + init(_ version: String) { + let phpConfigExecutablePath = "\(Paths.optPath)/php@\(version)/bin/php-config" + self.longVersion = version + if Shell.fileExists(phpConfigExecutablePath) { + self.longVersion = Command.execute( + path: phpConfigExecutablePath, + arguments: ["--version"] + ) } - // That's the long version - self.version.long = version - - // Next up, let's strip away the minor version number - let segments = self.version.long.components(separatedBy: ".") - - // Get the first two elements - self.version.short = segments[0...1].joined(separator: ".") - } - - /** - Retrieves the display value for a specific key in the `.ini` file. - - The following values are valid: - * -1: unlimited (show the infinity icon) - * 10000: an integer = amount of bytes - * 1K, 1M, 1G = shorthand for kilobytes, megabytes and gigabytes - - If none of these notations are used, the _fallback_ value is used. We'll show an emoji to indicate something has gone wrong here. - To clarify, B gets appended to valid values. As a result, "5M" (valid) becomes "5MB", and "5MB" (invalid) becomes ⚠️. - - - Parameter key: The key of the `ini` value that needs to be retrieved. For example, you can use `memory_limit`. - */ - private func getByteCount(key: String) -> String { - let value = Command.execute(path: Paths.php, arguments: ["-r", "echo ini_get('\(key)');"]) - - // Check if the value is unlimited - if (value == "-1") { - return "∞" - } - - // Check if the syntax is valid otherwise - let regex = try! NSRegularExpression(pattern: #"^([0-9]*)(K|M|G|)$"#, options: []) - let match = regex.matches(in: value, options: [], range: NSMakeRange(0, value.count)).first - return (match == nil) ? "⚠️" : "\(value)B" - } - - /** - It is always possible that the system configuration for PHP-FPM has not been set up for Valet. - This can occur when a user manually installs a new PHP version, but does not run `valet install`. - In that case, we should alert the user! - - - Important: The underlying check is `checkPhpFpmStatus`, which can be run multiple times. - This method actively presents a modal if said checks fails, so don't call this method too many times. - */ - public func notifyAboutBrokenPhpFpm() { - if !self.checkPhpFpmStatus() { - DispatchQueue.main.async { - Alert.notify( - message: "alert.php_fpm_broken.title".localized, - info: "alert.php_fpm_broken.info".localized - ) - } - } - } - - /** - Determine if PHP-FPM is configured correctly. - - For PHP 5.6, we'll check if `valet.sock` is included in the main `php-fpm.conf` file, but for more recent - versions of PHP, we can just check for the existence of the `valet-fpm.conf` file. If the check here fails, - that means that Valet won't work properly. - */ - private func checkPhpFpmStatus() -> Bool { - if self.version.short == "5.6" { - // The main PHP config file should contain `valet.sock` and then we're probably fine? - let fileName = "\(Paths.etcPath)/php/5.6/php-fpm.conf" - return Shell.pipe("cat \(fileName)").contains("valet.sock") - } - - // Make sure to check if valet-fpm.conf exists. If it does, we should be fine :) - return Shell.fileExists("\(Paths.etcPath)/php/\(self.version.short)/php-fpm.d/valet-fpm.conf") - } - - // MARK: - Structs - - struct Version { - var short = "???" - var long = "???" - var error = false - } - - struct Configuration { - var memory_limit = "???" - var upload_max_filesize = "???" - var post_max_size = "???" + let info = Shell.pipe("\(Paths.brew) info php@\(version) --json") + self.homebrewInfo = try! JSONDecoder().decode( + [HomebrewPackage].self, + from: info.data(using: .utf8)! + ).first! } } diff --git a/phpmon/Domain/Terminal/Shell.swift b/phpmon/Domain/Terminal/Shell.swift index a8b44ece..6cf482de 100644 --- a/phpmon/Domain/Terminal/Shell.swift +++ b/phpmon/Domain/Terminal/Shell.swift @@ -63,17 +63,23 @@ class Shell { */ func pipe(_ command: String) -> String { let task = Process() - let pipe = Pipe() + let outputPipe = Pipe() + let errorPipe = Pipe() task.launchPath = self.shell task.arguments = ["--login", "-c", command] - task.standardOutput = pipe + task.standardOutput = outputPipe + task.standardError = errorPipe task.launch() + + let error = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! + let output = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)! + + if (output == "" && error.lengthOfBytes(using: .utf8) > 0) { + return error + } - return String( - data: pipe.fileHandleForReading.readDataToEndOfFile(), - encoding: .utf8 - )! + return output } /** diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 885e8c95..444c25bf 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -102,6 +102,10 @@ "alert.cannot_start.close" = "Close"; "alert.cannot_start.retry" = "Retry"; +// PHP alias issue +"alert.php_alias_conflict.title" = "Homebrew `php` formula alias conflict detected"; +"alert.php_alias_conflict.info" = "PHP Monitor has detected conflicting `php` aliases in your Homebrew setup, both of which have been detected as installed.\n\nThis will likely result in failed linking when switching PHP versions, and will break PHP Monitor functionality.\n\nFor more information, please visit: https://github.com/nicoverbruggen/phpmon/issues/54"; + // STARTUP /// 1. PHP binary not found