diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index babc472..deb9dcb 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */; }; + C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */; }; C41C1B3722B0097F00E7CF16 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */; }; C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3A22B0098000E7CF16 /* Assets.xcassets */; }; C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C41C1B3C22B0098000E7CF16 /* Main.storyboard */; }; @@ -27,6 +29,8 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = InternetAccessPolicy.strings; sourceTree = ""; }; + C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = InternetAccessPolicy.plist; sourceTree = ""; }; C41C1B3322B0097F00E7CF16 /* PHP Monitor.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PHP Monitor.app"; sourceTree = BUILT_PRODUCTS_DIR; }; C41C1B3622B0097F00E7CF16 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C41C1B3A22B0098000E7CF16 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -61,6 +65,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C405A4CD24B9B9070062FAFA /* IAP */ = { + isa = PBXGroup; + children = ( + C405A4CF24B9B9140062FAFA /* InternetAccessPolicy.plist */, + C405A4CE24B9B9130062FAFA /* InternetAccessPolicy.strings */, + ); + path = IAP; + sourceTree = ""; + }; C41C1B2A22B0097F00E7CF16 = { isa = PBXGroup; children = ( @@ -91,6 +104,7 @@ C41C1B4022B0098000E7CF16 /* phpmon.entitlements */, C41C1B3A22B0098000E7CF16 /* Assets.xcassets */, C473319E2470923A009A0597 /* Localizable.strings */, + C405A4CD24B9B9070062FAFA /* IAP */, ); path = phpmon; sourceTree = ""; @@ -220,7 +234,9 @@ files = ( C41C1B3B22B0098000E7CF16 /* Assets.xcassets in Resources */, C41C1B3E22B0098000E7CF16 /* Main.storyboard in Resources */, + C405A4D124B9B9140062FAFA /* InternetAccessPolicy.plist in Resources */, C473319F2470923A009A0597 /* Localizable.strings in Resources */, + C405A4D024B9B9140062FAFA /* InternetAccessPolicy.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -385,7 +401,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = phpmon/Info.plist; @@ -393,7 +409,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.1; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -409,7 +425,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 23; + CURRENT_PROJECT_VERSION = 24; DEVELOPMENT_TEAM = 8M54J5J787; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = phpmon/Info.plist; @@ -417,7 +433,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 2.1; + MARKETING_VERSION = 2.2; PRODUCT_BUNDLE_IDENTIFIER = com.nicoverbruggen.phpmon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme new file mode 100644 index 0000000..c21a19b --- /dev/null +++ b/PHP Monitor.xcodeproj/xcshareddata/xcschemes/PHP Monitor.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index b96f39d..3365933 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # PHP Monitor +phpmon icon + PHP Monitor (or phpmon) is a lightweight macOS utility app that runs on your Mac and displays the active PHP version in your status bar. It also gives you quick access to various useful functionality (like switching PHP versions, restarting services, accessing configuration files, and more). @@ -12,7 +14,7 @@ It's also super convenient to and switch between versions. ## 🖥 System requirements -* macOS 10.15 Catalina +* macOS 10.15 Catalina or higher (works on macOS 11 Big Sur) * PHP 7.4 installed with Homebrew 2.x * Laravel Valet 2.x diff --git a/phpmon/Classes/Commands/Startup.swift b/phpmon/Classes/Commands/Startup.swift index e4f20a5..fa8e3d5 100644 --- a/phpmon/Classes/Commands/Startup.swift +++ b/phpmon/Classes/Commands/Startup.swift @@ -10,62 +10,102 @@ import Foundation class Startup { - public static func checkEnvironment() + public var failed : Bool = false + public var failureCallback = {} + + /** + Checks the user's environment and checks if PHP Monitor can be used properly. + This checks if PHP is installed, Valet is running, the appropriate permissions are set, and more. + + - Parameter success: Callback that is fired if the application can proceed with launch + - Parameter failure: Callback that is fired if the application must retry launch + */ + public func checkEnvironment(success: () -> Void, failure: @escaping () -> Void) { - self.presentAlertOnMainThreadIf( + self.failureCallback = failure + + self.performEnvironmentCheck( !Shell.user.pipe("which php").contains("/usr/local/bin/php"), messageText: "PHP is not correctly installed", - informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)" + informativeText: "You must install PHP via brew. Try running `which php` in Terminal, it should return `/usr/local/bin/php`. The app will not work correctly until you resolve this issue. (Usually `brew link php` resolves this issue.)", + breaking: true ) - self.presentAlertOnMainThreadIf( + self.performEnvironmentCheck( !Shell.user.pipe("ls /usr/local/opt | grep php@7.4").contains("php@7.4"), messageText: "PHP 7.4 is not correctly installed", - informativeText: "PHP 7.4 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php@7.4` in order for PHP Monitor to detect this installation." + informativeText: "PHP 7.4 alias was not found in `/usr/local/opt`. The app will not work correctly until you resolve this issue. If you already have the `php` formula installed, you may need to run `brew install php@7.4` in order for PHP Monitor to detect this installation.", + breaking: true ) - self.presentAlertOnMainThreadIf( + self.performEnvironmentCheck( !Shell.user.pipe("which valet").contains("/usr/local/bin/valet"), messageText: "Laravel Valet is not correctly installed", - informativeText: "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue." + informativeText: "You must install Valet with composer. Try running `which valet` in Terminal, it should return `/usr/local/bin/valet`. The app will not work correctly until you resolve this issue.", + breaking: true ) - self.presentAlertOnMainThreadIf( + self.performEnvironmentCheck( !Shell.user.pipe("cat /private/etc/sudoers.d/brew").contains("/usr/local/bin/brew"), messageText: "Brew has not been added to sudoers.d", - informativeText: "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." + informativeText: "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.", + breaking: true ) - self.presentAlertOnMainThreadIf( + self.performEnvironmentCheck( !Shell.user.pipe("cat /private/etc/sudoers.d/valet").contains("/usr/local/bin/valet"), messageText: "Valet has not been added to sudoers.d", - informativeText: "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." + informativeText: "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.", + breaking: true ) let services = Shell.user.pipe("brew services list | grep php") - self.presentAlertOnMainThreadIf( + self.performEnvironmentCheck( (services.countInstances(of: "started") > 1), messageText: "Multiple PHP services are active", informativeText: "This can cause php-fpm to serve a more recent version of PHP than the one you'd like to see active. Please terminate all extra PHP processes." + "\n\nThe easiest solution is to choose the option 'Force load latest PHP version' in the menu bar." + "\n\nAlternatively, you can fix this manually. You can do this by running `brew services list` and running `sudo brew services stop php@7.3` (and use the version that applies)." + "\n\nPHP Monitor usually handles the starting and stopping of these services, so once the correct version is the only PHP version running you should not have any issues. It is recommended to restart PHP Monitor once you have resolved this issue." + - "\n\nFor more information about this issue, please see the README.md file in the repository on GitHub." + "\n\nFor more information about this issue, please see the README.md file in the repository on GitHub.", + breaking: false ) + + if (!self.failed) { + success() + } } - private static func presentAlertOnMainThreadIf( + /** + * Perform an environment check. Will cause the application to terminate, if `breaking` is set to true. + * + * - Parameter condition: Condition to check for + * - Parameter messageText: Short description of what is wrong + * - Parameter informativeText: Expanded description of the environment check that failed + * - Parameter breaking: If the application should terminate afterwards + */ + private func performEnvironmentCheck( _ condition: Bool, messageText: String, - informativeText: String + informativeText: String, + breaking: Bool ) { if (condition) { + // Only breaking issues will cause the notification + if (breaking) { + self.failed = true + } DispatchQueue.main.async { - Alert.present( + // Present the information to the user + _ = Alert.present( messageText: messageText, informativeText: informativeText ) + // Only breaking issues will throw the extra retry modal + if (breaking) { + self.failureCallback() + } } } } diff --git a/phpmon/Classes/Helpers/Alert.swift b/phpmon/Classes/Helpers/Alert.swift index 149221f..1162223 100644 --- a/phpmon/Classes/Helpers/Alert.swift +++ b/phpmon/Classes/Helpers/Alert.swift @@ -12,12 +12,16 @@ class Alert { public static func present( messageText: String, informativeText: String, - buttonTitle: String = "OK" - ) { + buttonTitle: String = "OK", + secondButtonTitle: String = "" + ) -> Bool { let alert = NSAlert.init() alert.messageText = messageText alert.informativeText = informativeText alert.addButton(withTitle: buttonTitle) - alert.runModal() + if (!secondButtonTitle.isEmpty) { + alert.addButton(withTitle: secondButtonTitle) + } + return alert.runModal() == .alertFirstButtonReturn } } diff --git a/phpmon/IAP/InternetAccessPolicy.plist b/phpmon/IAP/InternetAccessPolicy.plist new file mode 100644 index 0000000..2c7c2e3 --- /dev/null +++ b/phpmon/IAP/InternetAccessPolicy.plist @@ -0,0 +1,47 @@ + + + + + ApplicationDescription + PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files. + DeveloperName + Nico Verbruggen + Website + https://github.com/nicoverbruggen/phpmon + Connections + + + IsIncoming + + Host + registry.npmjs.org + NetworkProtocol + TCP + Port + 80, 443 + Relevance + Essential + Purpose + PHP Monitor directly invokes Homebrew which contacts the NPM Registry. + DenyConsequences + If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail. + + + IsIncoming + + Host + github.com, api.github.com + NetworkProtocol + TCP + Port + 443 + Relevance + Essential + Purpose + PHP Monitor directly invokes Homebrew which contacts GitHub. + DenyConsequences + If you deny these connections, PHP Monitor might not be able to complete its preset set of instructions, causing version switching to fail. + + + + diff --git a/phpmon/IAP/InternetAccessPolicy.strings b/phpmon/IAP/InternetAccessPolicy.strings new file mode 100644 index 0000000..baf1ad1 --- /dev/null +++ b/phpmon/IAP/InternetAccessPolicy.strings @@ -0,0 +1,2 @@ +// Top-level, general application description: +"ApplicationDescription" = "PHP Monitor is a tool that shows the active PHP version in your menu bar and gives you easy access to certain PHP service actions and config files."; diff --git a/phpmon/Singletons/MainMenu.swift b/phpmon/Singletons/MainMenu.swift index 16d9cbb..429741c 100644 --- a/phpmon/Singletons/MainMenu.swift +++ b/phpmon/Singletons/MainMenu.swift @@ -21,18 +21,41 @@ class MainMenu: NSObject, NSWindowDelegate { self.setStatusBar(image: NSImage(named: NSImage.Name("StatusBarIcon"))!) // Perform environment boot checks DispatchQueue.global(qos: .userInitiated).async { [unowned self] in - Startup.checkEnvironment() - App.shared.availablePhpVersions = Actions.detectPhpVersions() - self.updatePhpVersionInStatusBar() - // Schedule a request to fetch the PHP version every 60 seconds - DispatchQueue.main.async { - App.shared.timer = Timer.scheduledTimer( - timeInterval: 60, - target: self, - selector: #selector(self.updatePhpVersionInStatusBar), - userInfo: nil, - repeats: true - ) + Startup().checkEnvironment(success: { + self.onEnvironmentPass() + }, failure: { + self.onEnvironmentFail() + }) + } + } + + private func onEnvironmentPass() { + App.shared.availablePhpVersions = Actions.detectPhpVersions() + self.updatePhpVersionInStatusBar() + // Schedule a request to fetch the PHP version every 60 seconds + DispatchQueue.main.async { + App.shared.timer = Timer.scheduledTimer( + timeInterval: 60, + target: self, + selector: #selector(self.updatePhpVersionInStatusBar), + userInfo: nil, + repeats: true + ) + } + } + + private func onEnvironmentFail() { + DispatchQueue.main.async { + let close = Alert.present( + messageText: "PHP Monitor cannot start", + informativeText: "The issue you were just notified about is keeping PHP Monitor from functioning correctly. Please fix the issue and restart PHP Monitor. After clicking on OK, PHP Monitor will close.\n\nIf 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.", + buttonTitle: "Close", + secondButtonTitle: "Retry" + ) + if (!close) { + self.startup() + } else { + exit(1) } } } @@ -140,12 +163,12 @@ class MainMenu: NSObject, NSWindowDelegate { } @objc public func forceRestartLatestPhp() { - Alert.present( + _ = Alert.present( messageText: "alert.force_reload.title".localized, informativeText: "alert.force_reload.info".localized ) self.waitAndExecute({ Actions.fixMyPhp() }, { - Alert.present( + _ = Alert.present( messageText: "alert.force_reload_done.title".localized, informativeText: "alert.force_reload_done.info".localized )