From 81ed154db144586bb81b63f9865242bc3fd45863 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 13 Mar 2023 21:03:13 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=20WIP:=20Discern=20installation=20?= =?UTF-8?q?status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Depending on the installation status, PHP Monitor should be able to find out which versions of PHP can be installed and removed. --- PHP Monitor.xcodeproj/project.pbxproj | 10 ++ phpmon/Common/Core/Actions.swift | 32 ---- phpmon/Common/PHP/PhpVersionInstaller.swift | 165 ++++++++++++++++++++ phpmon/Domain/Menu/MainMenu.swift | 8 +- phpmon/Domain/Menu/StatusMenu+Items.swift | 24 +++ phpmon/Domain/Menu/StatusMenu.swift | 6 - 6 files changed, 205 insertions(+), 40 deletions(-) create mode 100644 phpmon/Common/PHP/PhpVersionInstaller.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index 66b8627..48cf32b 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -735,6 +735,10 @@ 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 */; }; + C4F8764E29BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8764D29BFAF00006BBE26 /* PhpVersionInstaller.swift */; }; + C4F8764F29BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8764D29BFAF00006BBE26 /* PhpVersionInstaller.swift */; }; + C4F8765029BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8764D29BFAF00006BBE26 /* PhpVersionInstaller.swift */; }; + C4F8765129BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8764D29BFAF00006BBE26 /* PhpVersionInstaller.swift */; }; C4F8C0A422D4F12C002EFE61 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */; }; C4FACE80288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */; }; C4FACE81288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */; }; @@ -1005,6 +1009,7 @@ C4F7809B25D80344000DBC97 /* CommandTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandTest.swift; sourceTree = ""; }; C4F780A725D80AE8000DBC97 /* php.ini */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = php.ini; sourceTree = ""; }; C4F780AD25D80B37000DBC97 /* PhpExtensionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpExtensionTest.swift; sourceTree = ""; }; + C4F8764D29BFAF00006BBE26 /* PhpVersionInstaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhpVersionInstaller.swift; sourceTree = ""; }; C4F8C0A322D4F12C002EFE61 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; }; C4F8C0A522D4FA41002EFE61 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; C4FACE7F288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesWindowController+Hotkey.swift"; sourceTree = ""; }; @@ -1081,6 +1086,7 @@ 54B20EDF263AA22C00D3250E /* PHP */ = { isa = PBXGroup; children = ( + C4F8764D29BFAF00006BBE26 /* PhpVersionInstaller.swift */, C48D6C6E279CD29C00F26D7E /* PHP Version */, C4D9ADC2277610E4007277F4 /* Switcher */, C4F30B01278E169B00755FCE /* Homebrew */, @@ -2180,6 +2186,7 @@ C4205A7E27F4D21800191A39 /* ValetProxy.swift in Sources */, C4C8E818276F54D8003AC782 /* App+ConfigWatch.swift in Sources */, C4E49DE728F764050026AC4E /* ActiveCommand.swift in Sources */, + C4F8764E29BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */, 54FCFD30276C8DA4004CE748 /* HotkeyPreferenceView.swift in Sources */, C450C8C628C919EC002A2B4B /* PreferenceName.swift in Sources */, C4E4404627C56F4700D225E1 /* ValetSite.swift in Sources */, @@ -2456,6 +2463,7 @@ C471E80828F9BAD40021E251 /* PhpExtension.swift in Sources */, C471E7F928F9BACB0021E251 /* PhpSwitcher.swift in Sources */, C471E82A28F9BB330021E251 /* ValetListable.swift in Sources */, + C4F8765029BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */, C471E82728F9BB310021E251 /* HomebrewDiagnostics.swift in Sources */, C471E81C28F9BB250021E251 /* BetterAlert.swift in Sources */, C471E7DB28F9BA8F0021E251 /* RealShell.swift in Sources */, @@ -2642,6 +2650,7 @@ C471E80028F9BAD10021E251 /* Xdebug.swift in Sources */, C471E7F528F9BAC80021E251 /* PhpEnv.swift in Sources */, C471E7ED28F9BAC30021E251 /* Process.swift in Sources */, + C4F8765129BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */, C471E81128F9BAE80021E251 /* NSMenuItemExtension.swift in Sources */, C471E7CC28F9BA5B0021E251 /* TestableShell.swift in Sources */, C471E80C28F9BAE80021E251 /* NSWindowExtension.swift in Sources */, @@ -2681,6 +2690,7 @@ C485707128BF452E00539B36 /* WarningManager.swift in Sources */, C41CA5EE2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */, C4FACE81288F1C0D00FC478F /* PreferencesWindowController+Hotkey.swift in Sources */, + C4F8764F29BFAF00006BBE26 /* PhpVersionInstaller.swift in Sources */, C40934A3298EEB2C00D25014 /* CaskFile.swift in Sources */, 54D9E0B727E4F51E003B9AD9 /* HotKey.swift in Sources */, C413E43528DA3EB100AE33C7 /* TestableShellTest.swift in Sources */, diff --git a/phpmon/Common/Core/Actions.swift b/phpmon/Common/Core/Actions.swift index 8f17d70..38d33f3 100644 --- a/phpmon/Common/Core/Actions.swift +++ b/phpmon/Common/Core/Actions.swift @@ -130,36 +130,4 @@ class Actions { await brew("services restart \(Homebrew.Formulae.php)", sudo: Homebrew.Formulae.php.elevated) await brew("services restart \(Homebrew.Formulae.nginx)", sudo: Homebrew.Formulae.nginx.elevated) } - - public static func installPhpVersion(version: String) async { - let subject = ProgressViewSubject( - title: "Installing PHP \(version)", - description: "Please wait while Homebrew installs PHP \(version)..." - ) - - let installables = [ - "8.2": "php", - "8.1": "php@8.1", - "8.0": "php@8.0", - "7.4": "shivammathur/php/php@7.4", - "7.3": "shivammathur/php/php@7.3", - "7.2": "shivammathur/php/php@7.2", - "7.1": "shivammathur/php/php@7.1", - "7.0": "shivammathur/php/php@7.0" - ] - - if installables.keys.contains(version) { - let window = await ProgressWindowView.display(subject) - let formula = installables[version]! - if formula.contains("shivammathur") && !HomebrewDiagnostics.installedTaps.contains("shivammathur/php") { - await Shell.quiet("brew tap shivammathur/php") - } - // TODO: Attempt to read the progress of this - // Use the same way the composer progress is read - await brew("install \(formula)", sudo: false) - await PhpEnv.detectPhpVersions() - await MainMenu.shared.refreshActiveInstallation() - await window.close() - } - } } diff --git a/phpmon/Common/PHP/PhpVersionInstaller.swift b/phpmon/Common/PHP/PhpVersionInstaller.swift new file mode 100644 index 0000000..341397e --- /dev/null +++ b/phpmon/Common/PHP/PhpVersionInstaller.swift @@ -0,0 +1,165 @@ +// +// PhpVersionInstaller.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 13/03/2023. +// Copyright © 2023 Nico Verbruggen. All rights reserved. +// + +import Foundation + +public class PhpVersionInstaller { + public static var installables = [ + "8.2": "php", + "8.1": "php@8.1", + "8.0": "php@8.0", + "7.4": "shivammathur/php/php@7.4", + "7.3": "shivammathur/php/php@7.3", + "7.2": "shivammathur/php/php@7.2", + "7.1": "shivammathur/php/php@7.1", + "7.0": "shivammathur/php/php@7.0" + ] + + public enum PhpInstallAction { + case install + case remove + case purge + } + + // swiftlint:disable cyclomatic_complexity function_body_length + public static func modifyPhpVersion(version: String, action: PhpInstallAction) async { + let title = { + switch action { + case .install: + return "Installing PHP \(version)" + case .remove: + return "Removing PHP \(version)" + case .purge: + return "Purging PHP \(version)" + } + }() + + let description = { + switch action { + case .install: + return "Please wait while Homebrew installs PHP \(version)..." + case .remove: + return "Please wait while Homebrew uninstalls PHP \(version)..." + case .purge: + return "Please wait while Homebrew purges PHP \(version)" + } + }() + + let subject = ProgressViewSubject( + title: title, + description: description + ) + + let installables = Self.installables + + if installables.keys.contains(version) { + let window = await ProgressWindowView.display(subject) + let formula = installables[version]! + + var command = "" + + if action == .install { + if formula.contains("shivammathur") && !HomebrewDiagnostics.installedTaps.contains("shivammathur/php") { + await Shell.quiet("brew tap shivammathur/php") + } + } + + if action == .purge || action == .remove { + command = "brew remove \(formula) --force --ignore-dependencies" + + if action == .purge { + command += " --zap" + } + } + + let (process, _) = try! await Shell.attach( + command, + didReceiveOutput: { text, _ in + if action == .install { + // Check if we can recognize any of the typical progress steps + if let number = Self.reportInstallationProgress(text) { + Task { @MainActor in + subject.progress = number + } + } + } + }, + withTimeout: .minutes(5) + ) + + if process.terminationStatus <= 0 { + Task { @MainActor in + subject.progress = 100 + } + + await PhpEnv.detectPhpVersions() + await MainMenu.shared.refreshActiveInstallation() + Task { @MainActor in + subject.description = "The operation succeeded. This window will close in 5 seconds." + } + await window.close() + } else { + // Do not close the window and notify about failure + Task { @MainActor in + subject.description = "The operation failed." + } + } + } + } + + public static func installPhpVersion(version: String) async { + await self.modifyPhpVersion(version: version, action: .install) + } + + public static func removePhpVersion(version: String) async { + await self.modifyPhpVersion(version: version, action: .remove) + } + + private static func reportInstallationProgress(_ text: String) -> Double? { + if text.contains("Fetching") { + return 10 + } + if text.contains("Downloading") { + return 25 + } + if text.contains("Already downloaded") || text.contains("Downloaded") { + return 50 + } + if text.contains("Installing") { + return 60 + } + if text.contains("Pouring") { + return 80 + } + if text.contains("Summary") { + return 100 + } + return nil + } + + /** + Determine which action will be available in the PHP version manager. + Some versions will be available to be removed, some to be installed. + */ + public static var availableActions: [(version: String, action: PhpInstallAction)] { + var operations: [(version: String, action: PhpInstallAction)] = [] + + let installed = PhpEnv.shared.cachedPhpInstallations.keys + + for installable in installables.keys { + // While technically possible to uninstall the main formula (`php`) + // this should be disabled in the UI... this data should be correct though + operations.append((installable, installed.contains(installable) ? .remove : .install)) + } + + operations.sort { $1.version < $0.version } + + return operations + } + +} diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index 623b118..133bf4a 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -202,8 +202,12 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate Task { await AppUpdater().checkForUpdates(userInitiated: true) } } - @objc func installPhp74() { - Task { await Actions.installPhpVersion(version: "7.4") } + @objc func installPhpVersion(sender: PhpMenuItem) { + Task { await PhpVersionInstaller.installPhpVersion(version: sender.version) } + } + + @objc func removePhpVersion(sender: PhpMenuItem) { + Task { await PhpVersionInstaller.removePhpVersion(version: sender.version) } } // MARK: - Menu Delegate diff --git a/phpmon/Domain/Menu/StatusMenu+Items.swift b/phpmon/Domain/Menu/StatusMenu+Items.swift index 48954f9..a3a0a3e 100644 --- a/phpmon/Domain/Menu/StatusMenu+Items.swift +++ b/phpmon/Domain/Menu/StatusMenu+Items.swift @@ -91,6 +91,30 @@ extension StatusMenu { addItem(menuItem) } + // TODO: This is a fixed list... + + addItem(NSMenuItem.separator()) + addItem(HeaderView.asMenuItem(text: "Experimental")) + for result in PhpVersionInstaller.availableActions { + let title = result.action == .install + ? "Install PHP \(result.version)..." + : "Remove PHP \(result.version)..." + + var action: Selector? = result.action == .install + ? #selector(MainMenu.installPhpVersion(sender:)) + : #selector(MainMenu.removePhpVersion(sender:)) + + if result.version == PhpEnv.brewPhpAlias { + break + } + + addItem(PhpMenuItem( + title: title, + action: action, + keyEquivalent: "" + )) + } + if !PhpEnv.shared.incompatiblePhpVersions.isEmpty { addItem(NSMenuItem.separator()) addItem(NSMenuItem( diff --git a/phpmon/Domain/Menu/StatusMenu.swift b/phpmon/Domain/Menu/StatusMenu.swift index aa898b2..88d6623 100644 --- a/phpmon/Domain/Menu/StatusMenu.swift +++ b/phpmon/Domain/Menu/StatusMenu.swift @@ -65,12 +65,6 @@ class StatusMenu: NSMenu { addItem(NSMenuItem.separator()) - addItem(withTitle: "EXPERIMENTAL: Install PHP 7.4...", - action: #selector(MainMenu.installPhp74), - keyEquivalent: "") - - addItem(NSMenuItem.separator()) - addCoreMenuItems() } }