From 0c0e7fc87d7ad26b517f004ac91486b17692c408 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Wed, 7 Apr 2021 16:58:05 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Extension=20loading=20improvements?= =?UTF-8?q?=20(#31)=20and=20more?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * The README has been updated with additional information * The acknowledgements section has been added to the README * The php@X.X/opt/bin/php-config binary is now used (#39) * Extensions are now loaded from all possible .ini files * PHP Monitor's preferences window can now be triggered via hotkey * The first nine extensions can be triggered via hotkey --- README.md | 70 ++++++++++++++++++++++-- phpmon-tests/ExtensionParserTest.swift | 23 ++++++-- phpmon-tests/php.ini | 3 + phpmon/Domain/Core/PhpExtension.swift | 14 ++++- phpmon/Domain/Core/PhpInstallation.swift | 17 +++++- phpmon/Domain/Menu/MainMenu.swift | 2 +- phpmon/Domain/Menu/StatusMenu.swift | 18 ++++-- phpmon/Domain/Terminal/Actions.swift | 6 +- phpmon/Domain/Terminal/Command.swift | 10 +++- phpmon/Domain/Terminal/Paths.swift | 4 ++ phpmon/Localizable.strings | 2 +- 11 files changed, 147 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 6e6066c..7fae4ff 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,25 @@ PHP Monitor performs some integrity checks to ensure a good experience when usin > If you are having issues, the first thing you should be doing is installing the latest version of PHP Monitor _and_ Laravel Valet. This can resolve a variety of issues. To upgrade Valet, run `composer global update`. Don't forget to run `valet install` after upgrading. -If you're still having issues, here's a few common issues and solutions: +If you're still having issues, here's a few common questions & answers, as well as issues and solutions: + +
+Which versions of PHP are supported? + + + +For more details, consult the [constants file](https://github.com/nicoverbruggen/phpmon/blob/main/phpmon/Constants.swift#L16) file to see which versions are supported. + +
I want PHP Monitor to start up when I boot my Mac! @@ -135,6 +153,35 @@ This problem is usually resolved by upgrading Valet and running `valet install` valet install
+ +
+PHP Monitor tells me my installation is broken, but I don't see why! + +PHP Monitor tells you that a PHP installation is broken, if the configuration is causing warnings or errors when determining the version number. + +Since PHP Monitor changes the linked version via Homebrew, both Valet *and* your terminal (CLI) should use the new PHP version. + +However, this might not be the case on your system. You _might_ have a specific version of PHP linked if that is not the case. In that case, you may need to change your `.bashrc` or `.zshrc` file where the PATH is set (depending on the terminal you use). + +You can find out which version of PHP is being used by running `which php`. + +You can find out what exactly is causing the issue by running a command. On Intel, you can run (replace `7.4` with the version that is broken): + +``` +/usr/local/opt/php@7.4/bin/php -r "print phpversion();" +``` + +On Apple Silicon, you can run (replace `7.4` with the version that is broken): + +``` +/opt/homebrew/opt/php@7.4/bin/php -r "print phpversion();" +``` + +You should see an error or a warning here in the output. + +Usually this is a duplicate extension declaration causing issues, or an extension that couldn't be loaded. You'll have to solve that issue yourself (usually by removing the offending extension or reinstalling). + +
One of the limits (memory limit, max POST size, max upload size) shows an exclamation mark! @@ -150,7 +197,7 @@ You must a provide a value like so: `1024K`, `256M`, `1G`. Alternatively, `-1` i
One of my commented out extensions is not being detected... -The app searches in the relevant `php.ini` file for a specific pattern. For regular extensions: +The app searches in the relevant `.ini` files for a specific pattern. For regular extensions: * `extension="*.so"` * `; extension="*.so"` @@ -162,6 +209,8 @@ For Zend extensions: The `*` is a wildcard and the name of the extension. If you've commented out the extension, make sure you've commented it out with a semicolon (;) and a single space after the semicolon for PHP Monitor to detect it. +Since v3.4 all of the loaded .ini files are sourced to determine which extensions are enabled. +
@@ -185,7 +234,7 @@ PHP Monitor itself doesn't do any network requests. Feel free to check the sourc This is a security feature of Brew. When you start a service as an administrator, the root user becomes the owner of relevant binaries. -You will need to manually clean up those folders yourself using `rm -rf` or by manually removing those folders via Finder. +You will need to manually clean up those folders yourself using `rm -rf` (or by manually removing those folders via Finder).
@@ -201,11 +250,24 @@ You can find a [sponsor](https://nicoverbruggen.be/sponsor) link at the top of t Donations really help with the Apple Developer Program cost, and keep me motivated to keep working on PHP Monitor outside of work hours (I do have a day job!). +## 😎 Acknowledgements + +While I did make this application during my own free time, I have been lucky enough to do various experiments during work hours at [DIVE](https://dive.be). I'd also like to shout out the following folks: + +* My colleagues at [DIVE](https://dive.be) +* The [Homebrew](https://brew.sh/) team who maintain +* The [developers & maintainers of Valet](https://github.com/laravel/valet/graphs/contributors) +* Everyone in the Laravel community who shared the app (thanks!) +* Various folks who [reached](https://twitter.com/stauffermatt) [out](https://twitter.com/marcelpociot) +* Everyone who left feedback via issues + +Thank you very much for your contributions, kind words and support. + ## 🚜 How it works ### Loading info about PHP in the background -This utility runs `php -r 'print phpversion()'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit). +This utility runs `php-config --version'` in the background periodically. It also checks your `.ini` files for extensions and loads more information about your limits (memory limit, POST limit, upload limit). In order to save power, this only happens once every 60 seconds. diff --git a/phpmon-tests/ExtensionParserTest.swift b/phpmon-tests/ExtensionParserTest.swift index 5dc8db9..72565b7 100644 --- a/phpmon-tests/ExtensionParserTest.swift +++ b/phpmon-tests/ExtensionParserTest.swift @@ -23,24 +23,35 @@ class ExtensionParserTest: XCTestCase { func testExtensionNameIsCorrect() throws { let extensions = PhpExtension.load(from: Self.phpIniFileUrl) - XCTAssertEqual(extensions.first!.name, "xdebug") - XCTAssertEqual(extensions.last!.name, "imagick") + let extensionNames = extensions.map { (ext) -> String in + return ext.name + } + + XCTAssertTrue(extensionNames.contains("xdebug")) + XCTAssertTrue(extensionNames.contains("imagick")) + XCTAssertTrue(extensionNames.contains("opcache")) + XCTAssertTrue(extensionNames.contains("yaml")) + XCTAssertFalse(extensionNames.contains("fake")) } func testExtensionStatusIsCorrect() throws { let extensions = PhpExtension.load(from: Self.phpIniFileUrl) - XCTAssertEqual(extensions.first!.enabled, true) - XCTAssertEqual(extensions.last!.enabled, false) + // xdebug should be enabled + XCTAssertEqual(extensions[0].enabled, true) + + // imagick should be disabled + XCTAssertEqual(extensions[1].enabled, false) } func testToggleWorksAsExpected() throws { let destination = Utility.copyToTemporaryFile(resourceName: "php", fileExtension: "ini")! let extensions = PhpExtension.load(from: destination) - XCTAssertEqual(extensions.count, 2) + XCTAssertEqual(extensions.count, 4) - // Try to disable it! + // Try to disable xdebug (should be detected first)! let xdebug = extensions.first! + XCTAssertTrue(xdebug.name == "xdebug") XCTAssertEqual(xdebug.enabled, true) xdebug.toggle() XCTAssertEqual(xdebug.enabled, false) diff --git a/phpmon-tests/php.ini b/phpmon-tests/php.ini index 5afb2fe..e0128ba 100644 --- a/phpmon-tests/php.ini +++ b/phpmon-tests/php.ini @@ -1,5 +1,8 @@ zend_extension="xdebug.so" ; zend_extension="imagick.so" +zend_extension=/opt/homebrew/opt/php/lib/php/20200930/opcache.so +zend_extension="/opt/homebrew/opt/php/lib/php/20200930/yaml.so" +#zend_extension="/opt/homebrew/opt/php/lib/php/20200930/fake.so" [PHP] diff --git a/phpmon/Domain/Core/PhpExtension.swift b/phpmon/Domain/Core/PhpExtension.swift index 1925bae..0e38414 100644 --- a/phpmon/Domain/Core/PhpExtension.swift +++ b/phpmon/Domain/Core/PhpExtension.swift @@ -29,6 +29,11 @@ class PhpExtension { /// Whether the extension has been enabled. var enabled: Bool + /// The file where this extension was located, but only the filename, not the full path to the .ini file. + var fileNameOnly: String { + return String(file.split(separator: "/").last ?? "php.ini") + } + /** This regular expression will allow us to identify lines which activate an extension. @@ -41,7 +46,7 @@ class PhpExtension { - Note: Extensions that are disabled in a different way will not be detected. This is intentional. */ - static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)"(?[a-zA-Z]*).so"$"# + static let extensionRegex = #"^(extension=|zend_extension=|; extension=|; zend_extension=)(?["]?(?:\/?.\/?)+(?:\.so)"?)$"# /** When registering an extension, we do that based on the line found inside the .ini file. @@ -52,7 +57,12 @@ class PhpExtension { let range = Range(match!.range(withName: "name"), in: line)! self.line = line - self.name = line[range] + + let fullPath = String(line[range]) + .replacingOccurrences(of: "\"", with: "") // replace excess " + .replacingOccurrences(of: ".so", with: "") // replace excess .so + self.name = String(fullPath.split(separator: "/").last!) // take last segment + self.enabled = !line.contains(";") self.file = file } diff --git a/phpmon/Domain/Core/PhpInstallation.swift b/phpmon/Domain/Core/PhpInstallation.swift index 3c52a1f..cbc924a 100644 --- a/phpmon/Domain/Core/PhpInstallation.swift +++ b/phpmon/Domain/Core/PhpInstallation.swift @@ -34,7 +34,6 @@ class PhpInstallation { // Load extension information let path = URL(fileURLWithPath: "\(Paths.etcPath)/php/\(version.short)/php.ini") - extensions = PhpExtension.load(from: path) // Get configuration values @@ -43,6 +42,20 @@ class PhpInstallation { upload_max_filesize: Self.getByteCount(key: "upload_max_filesize"), post_max_size: Self.getByteCount(key: "post_max_size") ) + + // Determine which folder(s) to scan for additional files + let iniFolder = Command.execute(path: Paths.phpConfig, arguments: ["--ini-dir"], trimNewlines: true) + + // Check the contents of the ini dir + let enumerator = FileManager.default.enumerator(atPath: URL(fileURLWithPath: iniFolder).path) + let filePaths = enumerator?.allObjects as! [String] + + filePaths.filter { $0.contains(".ini") }.forEach { (iniFileName) in + let extensions = PhpExtension.load(from: URL(fileURLWithPath: "\(iniFolder)/\(iniFileName)")) + if extensions.count > 0 { + self.extensions.append(contentsOf: extensions) + } + } } /** @@ -51,7 +64,7 @@ class PhpInstallation { */ private static func getVersion() -> Version { var versionStruct = Version() - let version = Command.execute(path: Paths.php, arguments: ["-r", "print phpversion();"]) + let version = Command.execute(path: Paths.phpConfig, arguments: ["--version"], trimNewlines: true) if (version == "" || version.contains("Warning") || version.contains("Error")) { versionStruct.short = "💩 BROKEN" diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index b269323..9e03c8c 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -98,7 +98,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate { menu.addItem(NSMenuItem.separator()) // Add about & quit menu items - menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: "")) + menu.addItem(NSMenuItem(title: "mi_preferences".localized, action: #selector(openPrefs), keyEquivalent: ",")) menu.addItem(NSMenuItem(title: "mi_about".localized, action: #selector(openAbout), keyEquivalent: "")) menu.addItem(NSMenuItem(title: "mi_quit".localized, action: #selector(terminateApp), keyEquivalent: "q")) diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index b4466f7..5d2d7a9 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -106,8 +106,10 @@ class StatusMenu : NSMenu { self.addItem(NSMenuItem(title: "mi_no_extensions_detected".localized, action: nil, keyEquivalent: "")) } + var shortcutKey = 1 for phpExtension in App.phpInstall!.extensions { - self.addExtensionItem(phpExtension) + self.addExtensionItem(phpExtension, shortcutKey) + shortcutKey += 1 } self.addItem(NSMenuItem.separator()) @@ -115,11 +117,19 @@ class StatusMenu : NSMenu { self.addItem(NSMenuItem(title: "mi_php_refresh".localized, action: #selector(MainMenu.reloadPhpMonitorMenu), keyEquivalent: "r")) } - private func addExtensionItem(_ phpExtension: PhpExtension) { + private func addExtensionItem(_ phpExtension: PhpExtension, _ shortcutKey: Int) { + let keyEquivalent = shortcutKey < 9 ? "\(shortcutKey)" : "" + let menuItem = ExtensionMenuItem( - title: "\(phpExtension.name.capitalized) (php.ini)", - action: #selector(MainMenu.toggleExtension), keyEquivalent: "" + title: "\(phpExtension.name) (\(phpExtension.fileNameOnly))", + action: #selector(MainMenu.toggleExtension), + keyEquivalent: keyEquivalent ) + + if menuItem.keyEquivalent != "" { + menuItem.keyEquivalentModifierMask = [.option] + } + menuItem.state = phpExtension.enabled ? .on : .off menuItem.phpExtension = phpExtension diff --git a/phpmon/Domain/Terminal/Actions.swift b/phpmon/Domain/Terminal/Actions.swift index c917478..8fb4a6c 100644 --- a/phpmon/Domain/Terminal/Actions.swift +++ b/phpmon/Domain/Terminal/Actions.swift @@ -162,7 +162,11 @@ class Actions { */ public static func sed(file: String, original: String, replacement: String) { - Shell.run("sed -i '' 's/\(original)/\(replacement)/g' \(file)") + // Escape slashes (or `sed` won't work) + let e_original = original.replacingOccurrences(of: "/", with: "\\/") + let e_replacment = replacement.replacingOccurrences(of: "/", with: "\\/") + + Shell.run("sed -i '' 's/\(e_original)/\(e_replacment)/g' \(file)") } /** diff --git a/phpmon/Domain/Terminal/Command.swift b/phpmon/Domain/Terminal/Command.swift index a09cb09..28014a2 100644 --- a/phpmon/Domain/Terminal/Command.swift +++ b/phpmon/Domain/Terminal/Command.swift @@ -14,8 +14,9 @@ class Command { - Parameter path: The path of the command or program to invoke. - Parameter arguments: A list of arguments that are passed on. + - Parameter trimNewlines: Removes empty new line output. */ - public static func execute(path: String, arguments: [String]) -> String { + public static func execute(path: String, arguments: [String], trimNewlines: Bool = false) -> String { let task = Process() task.launchPath = path task.arguments = arguments @@ -26,6 +27,13 @@ class Command { let data = pipe.fileHandleForReading.readDataToEndOfFile() let output: String = String.init(data: data, encoding: String.Encoding.utf8)! + + if (trimNewlines) { + return output.components(separatedBy: .newlines) + .filter({ !$0.isEmpty }) + .joined(separator: "\n") + } + return output; } diff --git a/phpmon/Domain/Terminal/Paths.swift b/phpmon/Domain/Terminal/Paths.swift index 2b78567..77d4031 100644 --- a/phpmon/Domain/Terminal/Paths.swift +++ b/phpmon/Domain/Terminal/Paths.swift @@ -47,6 +47,10 @@ class Paths { return "\(binPath)/php" } + public static var phpConfig: String { + return "\(binPath)/php-config" + } + // - MARK: Paths public static var binPath: String { diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 26061a4..c26d952 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -45,7 +45,7 @@ // PREFERENCES -"prefs.title" = "Preferences"; +"prefs.title" = "PHP Monitor: Preferences"; "prefs.close" = "Close"; "prefs.dynamic_icon_title" = "Show a dynamic icon in the menu bar"; "prefs.dynamic_icon_desc" = "If you uncheck this box, the truck icon will always be visible.\nIf checked, it will display the major version number of the currently linked PHP version.";