From f413b84a45adfcf3ea7b8eb660d53663ec11a210 Mon Sep 17 00:00:00 2001 From: Nico Verbruggen Date: Mon, 9 May 2022 23:41:52 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=8F=97=20WIP:=20Check=20for=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PHP Monitor.xcodeproj/project.pbxproj | 6 + phpmon/Common/Core/Constants.swift | 4 + phpmon/Domain/App/Updater.swift | 120 ++++++++++++++++++ .../Homebrew/HomebrewDiagnostics.swift | 23 ++++ phpmon/Domain/Menu/MainMenu+Startup.swift | 13 +- phpmon/Domain/Menu/MainMenu.swift | 27 +--- phpmon/Domain/Preferences/Preferences.swift | 4 + phpmon/Domain/Preferences/PrefsVC.swift | 13 +- phpmon/Localizable.strings | 18 +++ 9 files changed, 199 insertions(+), 29 deletions(-) create mode 100644 phpmon/Domain/App/Updater.swift diff --git a/PHP Monitor.xcodeproj/project.pbxproj b/PHP Monitor.xcodeproj/project.pbxproj index da26ecd..feb31aa 100644 --- a/PHP Monitor.xcodeproj/project.pbxproj +++ b/PHP Monitor.xcodeproj/project.pbxproj @@ -117,6 +117,8 @@ C464ADAF275A7A69003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; }; C464ADB0275A7A6A003FCD53 /* DomainListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADAE275A7A69003FCD53 /* DomainListVC.swift */; }; C464ADB2275A87CA003FCD53 /* DomainListCellProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */; }; + C46E206D28299B3800D909D6 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E206C28299B3800D909D6 /* Updater.swift */; }; + C46E206E28299B3800D909D6 /* Updater.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46E206C28299B3800D909D6 /* Updater.swift */; }; C46FA23F246C358E00944F05 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C46FA23E246C358E00944F05 /* StringExtension.swift */; }; C473319F2470923A009A0597 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C473319E2470923A009A0597 /* Localizable.strings */; }; C47331A2247093B7009A0597 /* StatusMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C47331A1247093B7009A0597 /* StatusMenu.swift */; }; @@ -337,6 +339,7 @@ C464ADAB275A7A3F003FCD53 /* DomainListWC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListWC.swift; sourceTree = ""; }; C464ADAE275A7A69003FCD53 /* DomainListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListVC.swift; sourceTree = ""; }; C464ADB1275A87CA003FCD53 /* DomainListCellProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainListCellProtocol.swift; sourceTree = ""; }; + C46E206C28299B3800D909D6 /* Updater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Updater.swift; sourceTree = ""; }; C46FA23E246C358E00944F05 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = ""; }; C473319E2470923A009A0597 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; C47331A1247093B7009A0597 /* StatusMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusMenu.swift; sourceTree = ""; }; @@ -786,6 +789,7 @@ C4B97B7A275CF20A003F3378 /* App+GlobalHotkey.swift */, C4EED88827A48778006D7272 /* InterAppHandler.swift */, C4D8016522B1584700C6DA1B /* Startup.swift */, + C46E206C28299B3800D909D6 /* Updater.swift */, ); path = App; sourceTree = ""; @@ -1178,6 +1182,7 @@ C4811D2A22D70F9A00B5F6B3 /* MainMenu.swift in Sources */, C40C7F3027722E8D00DDDCDC /* Logger.swift in Sources */, C41CA5ED2774F8EE00A2C80E /* DomainListVC+Actions.swift in Sources */, + C46E206D28299B3800D909D6 /* Updater.swift in Sources */, C412E5FC25700D5300A1FB67 /* HomebrewPackage.swift in Sources */, C4D9ADBF277610E1007277F4 /* PhpSwitcher.swift in Sources */, C4068CAA27B0890D00544CD5 /* MenuBarIcons.swift in Sources */, @@ -1346,6 +1351,7 @@ C418898A275FE8CB001EF227 /* Filesystem.swift in Sources */, C4F780C625D80B75000DBC97 /* XibLoadable.swift in Sources */, C4EE55AA27708B9E001DF387 /* PMHeaderView.swift in Sources */, + C46E206E28299B3800D909D6 /* Updater.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/phpmon/Common/Core/Constants.swift b/phpmon/Common/Core/Constants.swift index 3420e9d..affe18c 100644 --- a/phpmon/Common/Core/Constants.swift +++ b/phpmon/Common/Core/Constants.swift @@ -65,6 +65,10 @@ struct Constants { string: "https://github.com/nicoverbruggen/phpmon#%EF%B8%8F-faq--troubleshooting" )! + static let GitHubReleases = URL( + string: "https://github.com/nicoverbruggen/phpmon/releases" + )! + static let StableBuildCaskFile = URL( string: "https://raw.githubusercontent.com/nicoverbruggen/homebrew-cask/master/Casks/phpmon.rb" )! diff --git a/phpmon/Domain/App/Updater.swift b/phpmon/Domain/App/Updater.swift new file mode 100644 index 0000000..53909c5 --- /dev/null +++ b/phpmon/Domain/App/Updater.swift @@ -0,0 +1,120 @@ +// +// Updater.swift +// PHP Monitor +// +// Created by Nico Verbruggen on 09/05/2022. +// Copyright © 2022 Nico Verbruggen. All rights reserved. +// + +import Foundation +import AppKit + +class Updater { + + public static var enabled: Bool = { + return Preferences.isEnabled(.automaticBackgroundUpdateCheck) + }() + + public static func checkForUpdates(background: Bool = true) { + // Information about the status of a potential background update + if background { + if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) { + Log.info("Automatic updates are disabled. No check will be performed.") + return + } else { + Log.info("Automatic updates are enabled, a check will be performed.") + } + } + + // Actually check for updates + let caskFile = App.version.contains("-dev") + ? Constants.Urls.DevBuildCaskFile.absoluteString + : Constants.Urls.StableBuildCaskFile.absoluteString + + // We'll find out what the new version is by using `curl` + var command = "curl -s" + + if background { + // If running as a background check, should only waste at most 2 secs of time + command = "curl -s --max-time 2" + } + + let versionString = Shell.pipe( + "\(command) '\(caskFile)' | grep version" + ) + + guard let onlineVersion = VersionExtractor.from(versionString) else { + Log.err("We couldn't check for updates!") + + // Only notify about connection issues if the request to check for updates was explicit + if !background { + notifyAboutConnectionIssue() + } + + return + } + + guard let current = VersionExtractor.from(App.shortVersion) else { + Log.err("We couldn't parse the current version number!") + return + } + + switch onlineVersion.versionCompare(current) { + case .orderedAscending: + Log.info("You are running a newer version of PHP Monitor.") + case .orderedDescending: + Log.info("There is a newer version (\(onlineVersion)) available!") + notifyAboutNewerVersion(version: onlineVersion) + case .orderedSame: + Log.info("The installed version \(current) matches the latest release (\(onlineVersion)).") + } + } + + private static func notifyAboutNewerVersion(version: String) { + let dev = App.version.contains("-dev") ? "-dev" : "" + + DispatchQueue.main.async { + BetterAlert().withInformation( + title: "updater.alerts.newer_version_available.title".localized(version), + subtitle: "updater.alerts.newer_version_available.subtitle".localized, + description: HomebrewDiagnostics.customCaskInstalled + ? "updater.installation_source.brew".localized + : "updater.installation_source.direct".localized + ) + .withPrimary( + text: "updater.alerts.buttons.release_notes".localized, + action: { vc in + vc.close(with: .OK) + NSWorkspace.shared.open( + Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version)\(dev)") + ) + } + ) + .withTertiary(text: "Close", action: { vc in + vc.close(with: .OK) + }) + .show() + } + } + + private static func notifyAboutConnectionIssue() { + DispatchQueue.main.async { + BetterAlert().withInformation( + title: "updater.errors.cannot_check_for_update.title".localized, + subtitle: "updater.errors.cannot_check_for_update.subtitle".localized, + description: "updater.errors.cannot_check_for_update.description".localized( + App.version + ) + ) + .withTertiary( + text: "updater.errors.buttons.releases_on_github".localized, + action: { _ in + NSWorkspace.shared.open(Constants.Urls.GitHubReleases) + } + ) + .withPrimary(text: "OK") + .show() + } + } + +} diff --git a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift b/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift index 67ab705..fc0b85d 100644 --- a/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift +++ b/phpmon/Domain/Integrations/Homebrew/HomebrewDiagnostics.swift @@ -10,6 +10,25 @@ import Foundation class HomebrewDiagnostics { + /** + Determines the Homebrew taps the user has installed. + */ + public static var installedTaps: [String] = { + return Shell + .pipe("\(Paths.brew) tap") + .split(separator: "\n") + .map { string in + return String(string) + } + }() + + /** + Determines whether the PHP Monitor Cask is installed. + */ + public static var customCaskInstalled: Bool = { + return installedTaps.contains("nicoverbruggen/cask") + }() + /** 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`). @@ -55,6 +74,10 @@ class HomebrewDiagnostics { } } + /** + This method, unsurprisingly, is supposed to show a specific alert about this alias conflict + with the `shivammathur/php` tap. + */ public static func presentAlertAboutConflict() { DispatchQueue.main.async { BetterAlert() diff --git a/phpmon/Domain/Menu/MainMenu+Startup.swift b/phpmon/Domain/Menu/MainMenu+Startup.swift index ce287f0..a3d3032 100644 --- a/phpmon/Domain/Menu/MainMenu+Startup.swift +++ b/phpmon/Domain/Menu/MainMenu+Startup.swift @@ -29,6 +29,12 @@ extension MainMenu { When the environment is all clear and the app can run, let's go. */ private func onEnvironmentPass() { + // Determine install method + Log.info(HomebrewDiagnostics.customCaskInstalled + ? "The app has probably been installed via Homebrew Cask." + : "The app has probably been installed directly." + ) + // Attempt to find out more info about Valet if Valet.shared.version != nil { Log.info("PHP Monitor has extracted the version number of Valet: \(Valet.shared.version!)") @@ -86,8 +92,6 @@ extension MainMenu { NotificationCenter.default.post(name: Events.ServicesUpdated, object: nil) - Log.info("PHP Monitor is ready to serve!") - // Schedule a request to fetch the PHP version every 60 seconds DispatchQueue.main.async { [self] in App.shared.timer = Timer.scheduledTimer( @@ -100,7 +104,12 @@ extension MainMenu { } Stats.incrementSuccessfulLaunchCount() + Stats.evaluateSponsorMessageShouldBeDisplayed() + + Updater.checkForUpdates() + + Log.info("PHP Monitor is ready to serve!") } /** diff --git a/phpmon/Domain/Menu/MainMenu.swift b/phpmon/Domain/Menu/MainMenu.swift index b77b3f0..903203d 100644 --- a/phpmon/Domain/Menu/MainMenu.swift +++ b/phpmon/Domain/Menu/MainMenu.swift @@ -339,32 +339,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate @objc func checkForUpdates() { DispatchQueue.global(qos: .userInitiated).async { - let caskFile = App.version.contains("-dev") - ? Constants.Urls.DevBuildCaskFile.absoluteString - : Constants.Urls.StableBuildCaskFile.absoluteString - - let versionString = Shell.pipe( - "curl -s '\(caskFile)' | grep version" - ) - - guard let onlineVersion = VersionExtractor.from(versionString) else { - Log.err("We couldn't check for updates!") - return - } - - guard let current = VersionExtractor.from(App.shortVersion) else { - Log.err("We couldn't parse the current version number!") - return - } - - switch onlineVersion.versionCompare(current) { - case .orderedAscending: - Log.info("You are running a newer version of PHP Monitor.") - case .orderedDescending: - Log.info("There is a newer version (\(onlineVersion)) available!") - case .orderedSame: - Log.info("The installed version matches the latest release (\(current)).") - } + Updater.checkForUpdates(background: false) } } diff --git a/phpmon/Domain/Preferences/Preferences.swift b/phpmon/Domain/Preferences/Preferences.swift index c4d5c7a..96b7937 100644 --- a/phpmon/Domain/Preferences/Preferences.swift +++ b/phpmon/Domain/Preferences/Preferences.swift @@ -20,6 +20,7 @@ enum PreferenceName: String { case autoComposerGlobalUpdateAfterSwitch = "auto_composer_global_update_after_switch" case allowProtocolForIntegrations = "allow_protocol_for_integrations" case globalHotkey = "global_hotkey" + case automaticBackgroundUpdateCheck = "backgroundUpdateCheck" } /** @@ -76,6 +77,7 @@ class Preferences { PreferenceName.autoServiceRestartAfterExtensionToggle.rawValue: true, PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue: false, PreferenceName.allowProtocolForIntegrations.rawValue: true, + PreferenceName.automaticBackgroundUpdateCheck.rawValue: true, /// Stats InternalStats.switchCount.rawValue: 0, InternalStats.launchCount.rawValue: 0, @@ -145,6 +147,8 @@ class Preferences { forKey: PreferenceName.autoComposerGlobalUpdateAfterSwitch.rawValue) as Any, .allowProtocolForIntegrations: UserDefaults.standard.bool( forKey: PreferenceName.allowProtocolForIntegrations.rawValue) as Any, + .automaticBackgroundUpdateCheck: UserDefaults.standard.bool( + forKey: PreferenceName.automaticBackgroundUpdateCheck.rawValue) as Any, // Part 2: Always Strings .globalHotkey: UserDefaults.standard.string( diff --git a/phpmon/Domain/Preferences/PrefsVC.swift b/phpmon/Domain/Preferences/PrefsVC.swift index a39230b..c7689cf 100644 --- a/phpmon/Domain/Preferences/PrefsVC.swift +++ b/phpmon/Domain/Preferences/PrefsVC.swift @@ -53,7 +53,8 @@ class PrefsVC: NSViewController { getAutoRestartPreferenceView(), getAutomaticComposerUpdatePreferenceView(), getShortcutPreferenceView(), - getIntegrationsPreferenceView() + getIntegrationsPreferenceView(), + getAutomaticUpdateCheckPreferenceView() ].forEach({ self.stackView.addArrangedSubview($0) }) } @@ -133,6 +134,16 @@ class PrefsVC: NSViewController { ) } + private func getAutomaticUpdateCheckPreferenceView() -> NSView { + return CheckboxPreferenceView.make( + sectionText: "prefs.updates".localized, + descriptionText: "prefs.automatic_update_check_desc".localized, + checkboxText: "prefs.automatic_update_check_title".localized, + preference: .automaticBackgroundUpdateCheck, + action: {} + ) + } + // MARK: - Listening for hotkey delegate var listeningForHotkeyView: HotkeyPreferenceView? diff --git a/phpmon/Localizable.strings b/phpmon/Localizable.strings index 903c0d4..fd8f623 100644 --- a/phpmon/Localizable.strings +++ b/phpmon/Localizable.strings @@ -184,6 +184,7 @@ "prefs.services" = "Services:"; "prefs.switcher" = "Switcher:"; "prefs.integrations" = "Integrations:"; +"prefs.updates" = "Updates:"; "prefs.icon_options.php" = "Display PHP Icon"; "prefs.icon_options.elephant" = "Display Elephant Icon"; @@ -205,6 +206,9 @@ "prefs.open_protocol_title" = "Allow third-party integrations"; "prefs.open_protocol_desc" = "When checked, this will allow the interaction with third party utilities to work (e.g. Alfred, Raycast). If you disable this, PHP Monitor will still receive the commands, but will not act upon them."; + +"prefs.automatic_update_check_title" = "Automatically check for updates"; +"prefs.automatic_update_check_desc" = "When checked, PHP Monitor will automatically check if there is a newer version available, and notify you if that is the case."; "prefs.shortcut_set" = "Set global shortcut"; "prefs.shortcut_listening" = ""; @@ -373,6 +377,20 @@ You can do this by running `composer global update` in your terminal. After that "alert.errors.homebrew_permissions.applescript_returned_nil.title" = "Restore Homebrew Permissions has been cancelled."; "alert.errors.homebrew_permissions.applescript_returned_nil.description" = "The outcome of the script that is executed to adjust the permissions returned nil, which usually means that you did not grant administrative permissions to PHP Monitor.\n\nIf you clicked on Cancel during the authentication prompt, this is normal. If you did actually authenticate and you are still seeing this message, something probably went wrong."; +// CHECK FOR UPDATES + +"updater.alerts.newer_version_available.title" = "A newer version of PHP Monitor (v%@) is now available!"; +"updater.alerts.newer_version_available.subtitle" = "Keeping PHP Monitor up-to-date is highly recommended, since newer versions usually fix bugs and include fixes to support the latest versions of Valet and PHP. Sometimes, there's even cool new features!"; +"updater.alerts.newer_version_available.description" = "PHP Monitor is supposed to be updated via Homebrew, so there is no built-in updater. This check is only meant to inform you of the existence of a new version, you do not need to upgrade."; +"updater.installation_source.brew" = "You appear to have installed PHP Monitor via Homebrew (or have at least tapped the required Caskfile) so it is recommended that you upgrade via the terminal by running `brew upgrade phpmon`."; +"updater.installation_source.direct" = "You do not appear to have installed PHP Monitor via Homebrew, so you will need to visit GitHub to download the latest update."; +"updater.alerts.buttons.release_notes" = "View Release Notes"; + +"updater.alerts.cannot_check_for_update.title" = "PHP Monitor could not determine if a newer version is available."; +"updater.alerts.cannot_check_for_update.subtitle" = "You might not be connected to the internet, are blocking traffic or GitHub is down and won't allow you to check for updates. If you keep seeing this message, you may want to manually check the releases page."; +"updater.alerts.cannot_check_for_update.description" = "The currently installed version is: %@. You can go to the list of the latest releases (on GitHub) by clicking on the button on the left."; +"updater.alerts.buttons.releases_on_github" = "View Releases"; + // WARNINGS ABOUT NON-DEFAULT TLD "alert.warnings.tld_issue.title" = "You are not using `.test` as the TLD for Valet.";