From c3e55df9fb43cd73e977979db3977d424937daa9 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 21 Nov 2022 23:55:37 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20WIP:=20Add=20support=20for=20`nginx?= =?UTF-8?q?-full`,=20formulae=20tweaks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 10 ++ phpmon/Common/Core/Actions.swift | 31 +++--- phpmon/Common/Core/Homebrew.swift | 37 +++++++ phpmon/Domain/App/App.swift | 3 - phpmon/Domain/App/ServicesManager.swift | 97 +++++++++++++------ phpmon/Domain/App/Startup.swift | 2 +- .../Homebrew/HomebrewDiagnostics.swift | 11 +++ phpmon/Domain/Menu/MainMenu+Startup.swift | 9 +- phpmon/Domain/SwiftUI/Menu/ServicesView.swift | 16 ++- 9 files changed, 153 insertions(+), 63 deletions(-) create mode 100644 phpmon/Common/Core/Homebrew.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 5ad4364..6b26180 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -555,6 +555,10 @@ C4C8E819276F54D8003AC782 /* App+ConfigWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */; }; C4C8E81B276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; C4C8E81C276F54E5003AC782 /* PhpConfigWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */; }; + C4CB6E65292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; + C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; + C4CB6E67292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; + C4CB6E68292C362C002E9027 /* Homebrew.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CB6E64292C362C002E9027 /* Homebrew.swift */; }; C4CCBA6C275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4CCBA6D275C567B008C7055 /* PMWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CCBA6B275C567B008C7055 /* PMWindowController.swift */; }; C4CDA893288F1A71007CE25F /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4CDA892288F1A71007CE25F /* Keys.swift */; }; @@ -862,6 +866,7 @@ C4C8900628F0E3EF00CE5E97 /* ActiveFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveFileSystem.swift; sourceTree = ""; }; C4C8E817276F54D8003AC782 /* App+ConfigWatch.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "App+ConfigWatch.swift"; sourceTree = ""; }; C4C8E81A276F54E5003AC782 /* PhpConfigWatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhpConfigWatcher.swift; sourceTree = ""; }; + C4CB6E64292C362C002E9027 /* Homebrew.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Homebrew.swift; sourceTree = ""; }; C4CCBA6B275C567B008C7055 /* PMWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PMWindowController.swift; sourceTree = ""; }; C4CDA892288F1A71007CE25F /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = ""; }; C4CE3BB727B31F2E0086CA49 /* MainMenu+Switcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainMenu+Switcher.swift"; sourceTree = ""; }; @@ -1083,6 +1088,7 @@ C4C1019A27C65C6F001FACC2 /* Process.swift */, C40C7F2F27722E8D00DDDCDC /* Logger.swift */, C417DC73277614690015E6EE /* Helpers.swift */, + C4CB6E64292C362C002E9027 /* Homebrew.swift */, ); path = Core; sourceTree = ""; @@ -2094,6 +2100,7 @@ C4B97B75275CF08C003F3378 /* AppDelegate+MenuOutlets.swift in Sources */, C41C02A627E60D7A009F26CB /* SiteScanner.swift in Sources */, C464ADAC275A7A3F003FCD53 /* DomainListWindowController.swift in Sources */, + C4CB6E65292C362C002E9027 /* Homebrew.swift in Sources */, C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */, C4EE188422D3386B00E126E5 /* Constants.swift in Sources */, C493084A279F331F009C240B /* AddSiteVC.swift in Sources */, @@ -2250,6 +2257,7 @@ C471E7E328F9BAC20021E251 /* Logger.swift in Sources */, C471E7FD28F9BACE0021E251 /* HomebrewService.swift in Sources */, C471E7E428F9BAC20021E251 /* Helpers.swift in Sources */, + C4CB6E67292C362C002E9027 /* Homebrew.swift in Sources */, C45E2A77291992DA005C7CFD /* FeatureTestCase.swift in Sources */, C471E82028F9BB290021E251 /* NginxConfigurationFile.swift in Sources */, C471E7D428F9BA8F0021E251 /* ActiveFileSystem.swift in Sources */, @@ -2413,6 +2421,7 @@ C471E7EA28F9BAC30021E251 /* Logger.swift in Sources */, C471E7FB28F9BACE0021E251 /* HomebrewService.swift in Sources */, C471E7EB28F9BAC30021E251 /* Helpers.swift in Sources */, + C4CB6E68292C362C002E9027 /* Homebrew.swift in Sources */, C4181F1128FAF9330042EA28 /* UITestCase.swift in Sources */, C471E81F28F9BB290021E251 /* NginxConfigurationFile.swift in Sources */, C471E7BF28F9B90F0021E251 /* StartupTest.swift in Sources */, @@ -2502,6 +2511,7 @@ C485707B28BF458900539B36 /* VersionPopoverView.swift in Sources */, C4E2E85D28FC282B003B070C /* TestableConfiguration.swift in Sources */, C485706E28BF451C00539B36 /* OnboardingWindowController.swift in Sources */, + C4CB6E66292C362C002E9027 /* Homebrew.swift in Sources */, C43603A1275E67610028EFC6 /* AppDelegate+Notifications.swift in Sources */, C4C3643A28AE4FCE00C0770E /* StatusMenu+Items.swift in Sources */, C42759682627662800093CAE /* NSMenuExtension.swift in Sources */, diff --git a/phpmon/Common/Core/Actions.swift b/phpmon/Common/Core/Actions.swift index 3963ded..ed62f17 100644 --- a/phpmon/Common/Core/Actions.swift +++ b/phpmon/Common/Core/Actions.swift @@ -13,31 +13,32 @@ class Actions { // MARK: - Services public static func restartPhpFpm() async { - await brew("services restart \(PhpEnv.phpInstall.formula)", sudo: true) + await brew("services restart \(Homebrew.Formulae.php.name)", sudo: Homebrew.Formulae.php.elevated) } public static func restartNginx() async { - await brew("services restart nginx", sudo: true) + await brew("services restart \(Homebrew.Formulae.nginx.name)", sudo: Homebrew.Formulae.nginx.elevated) } public static func restartDnsMasq() async { - await brew("services restart dnsmasq", sudo: true) + await brew("services restart \(Homebrew.Formulae.dnsmasq.name)", sudo: Homebrew.Formulae.dnsmasq.elevated) } public static func stopValetServices() async { - await brew("services stop \(PhpEnv.phpInstall.formula)", sudo: true) - await brew("services stop nginx", sudo: true) - await brew("services stop dnsmasq", sudo: true) + await brew("services stop \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated) + await brew("services stop \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated) + await brew("services stop \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated) } public static func fixHomebrewPermissions() throws { var servicesCommands = [ - "\(Paths.brew) services stop nginx", - "\(Paths.brew) services stop dnsmasq" + "\(Paths.brew) services stop \(Homebrew.Formulae.nginx)", + "\(Paths.brew) services stop \(Homebrew.Formulae.dnsmasq)" ] + var cellarCommands = [ - "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/nginx", - "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/dnsmasq" + "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.nginx)", + "chown -R \(Paths.whoami):admin \(Paths.cellarPath)/\(Homebrew.Formulae.dnsmasq)" ] PhpEnv.shared.availablePhpVersions.forEach { version in @@ -68,7 +69,7 @@ class Actions { public static func stopService(name: String) async { await brew( "services stop \(name)", - sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name } + sudo: ServicesManager.shared.services[name]?.formula.elevated ?? false ) await ServicesManager.loadHomebrewServices() } @@ -76,7 +77,7 @@ class Actions { public static func startService(name: String) async { await brew( "services start \(name)", - sudo: ServicesManager.shared.rootServices.contains { $0.value.name == name } + sudo: ServicesManager.shared.services[name]?.formula.elevated ?? false ) await ServicesManager.loadHomebrewServices() } @@ -138,9 +139,9 @@ class Actions { public static func fixMyValet(completed: @escaping () -> Void) { InternalSwitcher().performSwitch(to: PhpEnv.brewPhpAlias, completion: { Task { // Restart all services asynchronously and fire callback upon completion - await brew("services restart dnsmasq", sudo: true) - await brew("services restart php", sudo: true) - await brew("services restart nginx", sudo: true) + await brew("services restart \(Homebrew.Formulae.dnsmasq)", sudo: Homebrew.Formulae.dnsmasq.elevated) + await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated) + await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated) completed() } }) diff --git a/phpmon/Common/Core/Homebrew.swift b/phpmon/Common/Core/Homebrew.swift new file mode 100644 index 0000000..02f5e0f --- /dev/null +++ b/phpmon/Common/Core/Homebrew.swift @@ -0,0 +1,37 @@ +// +// Homebrew.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 21/11/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation + +class Homebrew { + struct Formulae { + static var php: HomebrewFormula { + return HomebrewFormula(PhpEnv.phpInstall.formula, elevated: true) + } + + static var nginx: HomebrewFormula { + return HomebrewDiagnostics.usesNginxFullFormula + ? HomebrewFormula("nginx-full", elevated: true) + : HomebrewFormula("nginx", elevated: true) + } + + static var dnsmasq: HomebrewFormula { + return HomebrewFormula("dnsmasq", elevated: true) + } + } +} + +class HomebrewFormula { + let name: String + let elevated: Bool + + init(_ name: String, elevated: Bool = true) { + self.name = name + self.elevated = elevated + } +} diff --git a/phpmon/Domain/App/App.swift b/phpmon/Domain/App/App.swift index 0b7da2a..491bd75 100644 --- a/phpmon/Domain/App/App.swift +++ b/phpmon/Domain/App/App.swift @@ -83,9 +83,6 @@ class App { /** List of detected (installed) applications that PHP Monitor can work with. */ var detectedApplications: [Application] = [] - /** The services manager, responsible for figuring out what services are active/inactive. */ - var services = ServicesManager.shared - /** The warning manager, responsible for keeping track of warnings. */ var warnings = WarningManager.shared diff --git a/phpmon/Domain/App/ServicesManager.swift b/phpmon/Domain/App/ServicesManager.swift index 08e46ef..02ec464 100644 --- a/phpmon/Domain/App/ServicesManager.swift +++ b/phpmon/Domain/App/ServicesManager.swift @@ -13,45 +13,78 @@ class ServicesManager: ObservableObject { static var shared = ServicesManager() - @Published var rootServices: [String: HomebrewService] = [:] - @Published var userServices: [String: HomebrewService] = [:] + private var formulae: [HomebrewFormula] - public static func loadHomebrewServices() async { - let rootServiceNames = [ - PhpEnv.phpInstall.formula, - "nginx", - "dnsmasq" + @Published var services: [String: ServiceWrapper] = [:] + + init() { + formulae = [ + Homebrew.Formulae.php, + Homebrew.Formulae.nginx, + Homebrew.Formulae.dnsmasq ] - let normalJson = await Shell - .pipe("sudo \(Paths.brew) services info --all --json") - .out.data(using: .utf8)! + let additionalFormulae = (Preferences.custom.services ?? []).map({ item in + return HomebrewFormula(item, elevated: false) + }) - let normalServices = try! JSONDecoder() - .decode([HomebrewService].self, from: normalJson) - .filter({ return rootServiceNames.contains($0.name) }) + formulae.append(contentsOf: additionalFormulae) - Task { @MainActor in - ServicesManager.shared.rootServices = Dictionary( - uniqueKeysWithValues: normalServices.map { ($0.name, $0) } - ) + services = Dictionary(uniqueKeysWithValues: formulae.map { ($0.name, ServiceWrapper(formula: $0)) }) + } + + public static func loadHomebrewServices() async { + Task { + let rootServiceNames = Self.shared.formulae + .filter { $0.elevated } + .map { $0.name } + + let rootJson = await Shell + .pipe("sudo \(Paths.brew) services info --all --json") + .out.data(using: .utf8)! + + let rootServices = try! JSONDecoder() + .decode([HomebrewService].self, from: rootJson) + .filter({ return rootServiceNames.contains($0.name) }) + + Task { @MainActor in + for service in rootServices { + Self.shared.services[service.name]!.service = service + } + } } - guard let userServiceNames = Preferences.custom.services else { return } + Task { + let userServiceNames = Self.shared.formulae + .filter { !$0.elevated } + .map { $0.name } - let rootJson = await Shell - .pipe("\(Paths.brew) services info --all --json") - .out - .data(using: .utf8)! + let normalJson = await Shell + .pipe("\(Paths.brew) services info --all --json") + .out.data(using: .utf8)! - let rootServices = try! JSONDecoder() - .decode([HomebrewService].self, from: rootJson) - .filter({ return userServiceNames.contains($0.name) }) + let userServices = try! JSONDecoder() + .decode([HomebrewService].self, from: normalJson) + .filter({ return userServiceNames.contains($0.name) }) - Task { @MainActor in - ServicesManager.shared.userServices = Dictionary( - uniqueKeysWithValues: rootServices.map { ($0.name, $0) } - ) + Task { @MainActor in + for service in userServices { + Self.shared.services[service.name]!.service = service + } + } + } + } + + /** + Service wrapper, that contains the Homebrew JSON output (if determined) and the formula. + This helps the app determine whether a service should run as an administrator or not. + */ + public class ServiceWrapper { + public let formula: HomebrewFormula + public var service: HomebrewService? + + init(formula: HomebrewFormula) { + self.formula = formula } } @@ -60,11 +93,11 @@ class ServicesManager: ObservableObject { */ func withDummyServices(_ services: [String: Bool]) -> Self { for (service, enabled) in services { - let item = HomebrewService.dummy(named: service, enabled: enabled) - self.rootServices[service] = item + let item = ServiceWrapper(formula: HomebrewFormula(service)) + item.service = HomebrewService.dummy(named: service, enabled: enabled) + self.services[service] = item } return self } - } diff --git a/phpmon/Domain/App/Startup.swift b/phpmon/Domain/App/Startup.swift index be840db..5d53b78 100644 --- a/phpmon/Domain/App/Startup.swift +++ b/phpmon/Domain/App/Startup.swift @@ -162,7 +162,7 @@ class Startup { EnvironmentCheck( command: { await HomebrewDiagnostics.loadInstalledTaps() - return await HomebrewDiagnostics.cannotLoadService("nginx") + return await HomebrewDiagnostics.cannotLoadService("dnsmasq") }, name: "`sudo \(Paths.brew) services info` JSON loaded", titleText: "startup.errors.services_json_error.title".localized, diff --git a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift b/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift index 21ce04a..4309ff8 100644 --- a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift +++ b/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift @@ -34,6 +34,17 @@ class HomebrewDiagnostics { return installedTaps.contains("nicoverbruggen/cask") }() + /** + Determines whether to use the regular `nginx` or `nginx-full` formula. + */ + public static var usesNginxFullFormula: Bool = { + guard let destination = try? FileManager.default + .destinationOfSymbolicLink(atPath: "\(Paths.binPath)/nginx") else { return false } + + // Verify that the `nginx` binary is symlinked to a directory that includes `nginx-full`. + return destination.contains("/nginx-full/") + }() + /** 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`). diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index 9a87858..2e1e30f 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -36,8 +36,13 @@ extension MainMenu { // Determine install method Log.info(HomebrewDiagnostics.customCaskInstalled - ? "The app has probably been installed via Homebrew Cask." - : "The app has probably been installed directly." + ? "[BREW] The app has probably been installed via Homebrew Cask." + : "[BREW] The app has probably been installed directly." + ) + + Log.info(HomebrewDiagnostics.usesNginxFullFormula + ? "[BREW] The app will be using the `nginx-full` formula." + : "[BREW] The app will be using the `nginx` formula." ) // Attempt to find out more info about Valet diff --git a/phpmon/Domain/SwiftUI/Menu/ServicesView.swift b/phpmon/Domain/SwiftUI/Menu/ServicesView.swift index 1b09703..abfe51e 100644 --- a/phpmon/Domain/SwiftUI/Menu/ServicesView.swift +++ b/phpmon/Domain/SwiftUI/Menu/ServicesView.swift @@ -17,9 +17,9 @@ struct ServicesView: View { static func asMenuItem(perRow: Int = 3) -> NSMenuItem { let item = NSMenuItem() var services = [ - PhpEnv.phpInstall.formula, - "nginx", - "dnsmasq" + Homebrew.Formulae.php.name, + Homebrew.Formulae.nginx.name, + Homebrew.Formulae.dnsmasq.name ] if Preferences.custom.hasServices() { @@ -76,16 +76,12 @@ struct CheckmarkView: View { @EnvironmentObject var manager: ServicesManager public func hasAnyServices() -> Bool { - return !manager.rootServices.isEmpty + return !manager.services.isEmpty } public func active() -> Bool? { - if manager.rootServices.keys.contains(serviceName) { - return manager.rootServices[serviceName]!.running - } - - if manager.userServices.keys.contains(serviceName) { - return manager.userServices[serviceName]!.running + if manager.services.keys.contains(serviceName) { + return manager.services[serviceName]!.service?.running ?? nil } return nil