1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-11-07 05:10:06 +01:00

♻️ Reworked updater

This commit is contained in:
2023-02-05 17:58:44 +01:00
parent 78e682688b
commit 300880f3e5
10 changed files with 130 additions and 289 deletions

View File

@@ -1,228 +0,0 @@
//
// Updater.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 09/05/2022.
// Copyright © 2023 Nico Verbruggen. All rights reserved.
//
import Foundation
import AppKit
class AppUpdateChecker {
public static var latestCaskFileContents: String = ""
public static var enabled: Bool = {
return Preferences.isEnabled(.automaticBackgroundUpdateCheck)
}()
public static var isDev: Bool = {
return App.version.contains("-dev")
}()
public static func retrieveVersionFromCask(
_ initiatedFromBackground: Bool = true
) async -> String {
let caskFile = App.version.contains("-dev")
? Constants.Urls.DevBuildCaskFile.absoluteString
: Constants.Urls.StableBuildCaskFile.absoluteString
var command = "curl -s"
if initiatedFromBackground {
command = "curl -s --max-time 5"
}
AppUpdateChecker.latestCaskFileContents = await Shell.pipe("\(command) '\(caskFile)'").out
return await Shell.pipe("echo \"\(Self.latestCaskFileContents)\" | grep version").out
}
public static func checkIfNewerVersionIsAvailable(
initiatedFromBackground: Bool = true
) async {
if initiatedFromBackground {
if !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Automatic updates are disabled. No check will be performed.")
return
}
Log.info("Automatic updates are enabled, a check will be performed.")
}
let versionString = await retrieveVersionFromCask(initiatedFromBackground)
guard let onlineVersion = AppVersion.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 !initiatedFromBackground {
notifyAboutConnectionIssue()
}
return
}
let currentVersion = AppVersion.fromCurrentVersion()
handleVersionComparison(
currentVersion,
onlineVersion,
initiatedFromBackground
)
}
private static func handleVersionComparison(
_ currentVersion: AppVersion,
_ onlineVersion: AppVersion,
_ background: Bool
) {
// TODO: Restore original behaviour
notifyAboutNewerVersion(version: onlineVersion)
return
switch onlineVersion.version.versionCompare(currentVersion.version) {
case .orderedAscending:
Log.info("You are running a newer version of PHP Monitor "
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
case .orderedDescending:
Log.info("There is a newer version (\(onlineVersion)) available! "
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
notifyAboutNewerVersion(version: onlineVersion)
case .orderedSame:
if currentVersion.build != nil
&& onlineVersion.build != nil
&& buildDiffers(currentVersion, onlineVersion, background) {
return
}
Log.info("The installed version (\(currentVersion.computerReadable)) matches the latest release "
+ "(\(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
}
}
private static func buildDiffers(
_ currentVersion: AppVersion,
_ onlineVersion: AppVersion,
_ background: Bool
) -> Bool {
if onlineVersion.build! > currentVersion.build! {
Log.info("There is a newer build of PHP Monitor available! "
+ "(\(onlineVersion.computerReadable) > \(currentVersion.computerReadable))")
notifyAboutNewerVersion(version: onlineVersion)
return true
} else if onlineVersion.build! < currentVersion.build! {
Log.info("You are running a newer build of PHP Monitor "
+ "(\(currentVersion.computerReadable) > \(onlineVersion.computerReadable)).")
if !background { notifyVersionDoesNotNeedUpgrade() }
return true
}
return false
}
private static func notifyVersionDoesNotNeedUpgrade() {
Task { @MainActor in
BetterAlert().withInformation(
title: "updater.alerts.is_latest_version.title".localized,
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
description: ""
)
.withPrimary(text: "generic.ok".localized)
.show()
}
}
private static func notifyAboutNewerVersion(version: AppVersion) {
let devSuffix = isDev ? "-dev" : ""
let command = isDev ? "brew upgrade phpmon-dev" : "brew upgrade phpmon"
Task { @MainActor in
BetterAlert().withInformation(
title: "updater.alerts.newer_version_available.title".localized(version.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle".localized,
description: HomebrewDiagnostics.customCaskInstalled
? "updater.installation_source.brew".localized(command)
: "updater.installation_source.direct".localized
)
.withPrimary(
text: "updater.alerts.buttons.install".localized,
action: { vc in
Self.installUpdate()
vc.close(with: .OK)
}
)
.withSecondary(
text: "updater.alerts.buttons.release_notes".localized,
action: { vc in
vc.close(with: .OK)
NSWorkspace.shared.open(
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/v\(version.tagged)\(devSuffix)")
)
}
)
.withTertiary(text: "Dismiss", action: { vc in
vc.close(with: .OK)
})
.show()
}
}
private static func installUpdate() {
let updater = Bundle.main.resourceURL!.path + "/PHP Monitor Self-Updater.app"
system_quiet("mkdir -p ~/.config/phpmon/updater 2> /dev/null")
let updaterDirectory = "~/.config/phpmon/updater"
.replacingOccurrences(of: "~", with: NSHomeDirectory())
system_quiet("cp -R \"\(updater)\" \"\(updaterDirectory)/PHP Monitor Self-Updater.app\"")
let sha256 = system("echo \"\(Self.latestCaskFileContents)\" | grep sha256")
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "'", with: "")
.split(separator: " ").last ?? ""
let url = system("echo \"\(Self.latestCaskFileContents)\" | grep url")
.trimmingCharacters(in: .whitespacesAndNewlines)
.replacingOccurrences(of: "'", with: "")
.split(separator: " ").last ?? ""
try! FileSystem.writeAtomicallyToFile(
"\(updaterDirectory)/update.json",
content: """
{ "url": "\(url)", "sha256": "\(sha256)" }
"""
)
let updaterUrl = NSURL(fileURLWithPath: updater, isDirectory: true) as URL
let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: updaterUrl, configuration: configuration) { _, _ in
print("The updater has been launched successfully!")
}
}
private static func notifyAboutConnectionIssue() {
Task { @MainActor in
BetterAlert().withInformation(
title: "updater.alerts.cannot_check_for_update.title".localized,
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
description: "updater.alerts.cannot_check_for_update.description".localized(
App.version
)
)
.withTertiary(
text: "updater.alerts.buttons.releases_on_github".localized,
action: { _ in
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}
)
.withPrimary(text: "generic.ok".localized)
.show()
}
}
}

View File

@@ -7,75 +7,160 @@
//
import Foundation
import Cocoa
class AppUpdater {
var caskFile: CaskFile!
var latestVersionOnline: AppVersion!
var interactive: Bool = false
public func checkForUpdates(background: Bool) async {
if background && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
public func checkForUpdates(interactive: Bool) async {
self.interactive = interactive
if interactive && !Preferences.isEnabled(.automaticBackgroundUpdateCheck) {
Log.info("Skipping automatic update check due to user preference.")
return
}
Log.info("The app will search for updates...")
let caskUrl = App.version.contains("-dev")
let caskUrl = App.identifier.contains(".dev")
? Constants.Urls.DevBuildCaskFile
: Constants.Urls.StableBuildCaskFile
guard let caskFile = await CaskFile.from(url: caskUrl) else {
Log.err("The contents of the CaskFile at '\(caskUrl.absoluteString)' could not be retrieved.")
if !background {
return presentCouldNotRetrieveUpdate()
} else {
return
}
return presentCouldNotRetrieveUpdateIfInteractive()
}
self.caskFile = caskFile
if newerVersionExists() {
presentNewerVersionAvailableAlert()
} else {
if !background {
presentNoNewerVersionAvailableAlert()
}
}
}
var caskFile: CaskFile!
public func newerVersionExists() -> Bool {
let currentVersion = AppVersion.fromCurrentVersion()
guard let onlineVersion = AppVersion.from(caskFile.version) else {
Log.err("The version string from the CaskFile could not be read.")
return false
return presentCouldNotRetrieveUpdateIfInteractive()
}
Log.info("You are running \(currentVersion.computerReadable). The latest version is: \(onlineVersion.computerReadable).")
latestVersionOnline = onlineVersion
Log.info("The latest version read from '\(caskUrl.lastPathComponent)' is: v\(onlineVersion.computerReadable).")
// Do the comparison w/ current version
return true
if latestVersionOnline > currentVersion {
presentNewerVersionAvailableAlert()
} else if interactive {
presentNoNewerVersionAvailableAlert()
}
}
private func presentCouldNotRetrieveUpdateIfInteractive() {
if interactive {
return presentCouldNotRetrieveUpdate()
} else {
return
}
}
// MARK: - Alerts
public func presentNewerVersionAvailableAlert() {
print("A newer version is available")
let command = App.identifier.contains(".dev")
? "brew upgrade phpmon-dev"
: "brew upgrade phpmon"
Task { @MainActor in
BetterAlert().withInformation(
title: "updater.alerts.newer_version_available.title"
.localized(latestVersionOnline.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle"
.localized,
description: HomebrewDiagnostics.customCaskInstalled
? "updater.installation_source.brew".localized(command)
: "updater.installation_source.direct".localized
)
.withPrimary(
text: "updater.alerts.buttons.install".localized,
action: { vc in
self.prepareForDownload()
vc.close(with: .OK)
}
)
.withSecondary(
text: "updater.alerts.buttons.release_notes".localized,
action: { _ in
let urlSegments = self.caskFile.url.split(separator: "/")
let tag = urlSegments[urlSegments.count - 2] // ../download/{tag}/{file.zip}
NSWorkspace.shared.open(
Constants.Urls.GitHubReleases.appendingPathComponent("/tag/\(tag)")
)
}
)
.withTertiary(text: "updater.alerts.buttons.dismiss".localized, action: { vc in
vc.close(with: .OK)
})
.show()
}
}
public func presentNoNewerVersionAvailableAlert() {
print("No newer version is available")
Task { @MainActor in
BetterAlert().withInformation(
title: "updater.alerts.is_latest_version.title".localized,
subtitle: "updater.alerts.is_latest_version.subtitle".localized(App.shortVersion),
description: ""
)
.withPrimary(text: "generic.ok".localized)
.show()
}
}
public func presentCouldNotRetrieveUpdate() {
print("Could not retrieve update")
Task { @MainActor in
BetterAlert().withInformation(
title: "updater.alerts.cannot_check_for_update.title".localized,
subtitle: "updater.alerts.cannot_check_for_update.subtitle".localized,
description: "updater.alerts.cannot_check_for_update.description".localized(
App.version
)
)
.withTertiary(
text: "updater.alerts.buttons.releases_on_github".localized,
action: { _ in
NSWorkspace.shared.open(Constants.Urls.GitHubReleases)
}
)
.withPrimary(text: "generic.ok".localized)
.show()
}
}
// MARK: - Preparing for Self-Updater
private func prepareForDownload() {
let updater = Bundle.main.resourceURL!.path + "/PHP Monitor Self-Updater.app"
system_quiet("mkdir -p ~/.config/phpmon/updater 2> /dev/null")
let updaterDirectory = "~/.config/phpmon/updater"
.replacingOccurrences(of: "~", with: NSHomeDirectory())
system_quiet("cp -R \"\(updater)\" \"\(updaterDirectory)/PHP Monitor Self-Updater.app\"")
try! FileSystem.writeAtomicallyToFile(
"\(updaterDirectory)/update.json",
content: "{ \"url\": \"\(caskFile.url)\", \"sha256\": \"\(caskFile.sha256)\" }"
)
let updaterUrl = NSURL(fileURLWithPath: updater, isDirectory: true) as URL
let configuration = NSWorkspace.OpenConfiguration()
NSWorkspace.shared.openApplication(at: updaterUrl, configuration: configuration) { _, _ in
Log.info("The updater has been launched successfully!")
}
}
public static func checkIfUpgradeWasPerformed() {
// MARK: - Checking if Self-Updater Worked
public static func checkIfUpdateWasPerformed() {
if FileSystem.fileExists("~/.config/phpmon/updater/upgrade.success") {
// Send a notification about the update
Task { @MainActor in

View File

@@ -87,7 +87,7 @@ class AppVersion: Comparable {
static func < (lhs: AppVersion, rhs: AppVersion) -> Bool {
let comparisonResult = lhs.version.versionCompare(rhs.version)
if comparisonResult == .orderedDescending {
if comparisonResult == .orderedAscending {
return true
}

View File

@@ -75,5 +75,4 @@ struct CaskFile {
return CaskFile(properties: props)
}
}

View File

@@ -112,10 +112,7 @@ extension MainMenu {
}
}
// await AppUpdateChecker.checkIfNewerVersionIsAvailable()
await AppUpdater().checkForUpdates(background: true)
exit(0)
await AppUpdater().checkForUpdates(interactive: false)
}
// Check if the linked version has changed between launches of phpmon
@@ -125,7 +122,7 @@ extension MainMenu {
Log.info("PHP Monitor is ready to serve!")
// Check if we upgraded just now
AppUpdater.checkIfUpgradeWasPerformed()
AppUpdater.checkIfUpdateWasPerformed()
}
/**

View File

@@ -199,7 +199,7 @@ class MainMenu: NSObject, NSWindowDelegate, NSMenuDelegate, PhpSwitcherDelegate
}
@objc func checkForUpdates() {
Task { await AppUpdateChecker.checkIfNewerVersionIsAvailable(initiatedFromBackground: false) }
Task { await AppUpdater().checkForUpdates(interactive: true) }
}
// MARK: - Menu Delegate

View File

@@ -623,8 +623,8 @@ COMMON TROUBLESHOOTING TIPS
"updater.alerts.newer_version_available.title" = "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.";
"updater.installation_source.brew" = "The recommended method of installing updates to PHP Monitor is to simply press 'Install Update'. This will launch the separate PHP Monitor Self-Updater, which will update the app. You may also upgrade via the terminal by running `%@`.";
"updater.installation_source.direct" = "The recommended method of installing updates to PHP Monitor is to simply press 'Install Update'. This will launch the separate PHP Monitor Self-Updater, which will update the app.";
"updater.installation_source.brew" = "The recommended method of installing updates to PHP Monitor is to simply press 'Install Update'.\n\n(You may also upgrade via the terminal by running `%@`, but this is not recommended.)";
"updater.installation_source.direct" = "The recommended method of installing updates to PHP Monitor is to simply press 'Install Update'.";
"updater.alerts.buttons.release_notes" = "View Release Notes";
"updater.alerts.is_latest_version.title" = "PHP Monitor is up-to-date!";
@@ -635,6 +635,7 @@ COMMON TROUBLESHOOTING TIPS
"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";
"updater.alerts.buttons.install" = "Install Update";
"updater.alerts.buttons.dismiss" = "Dismiss";
// WARNINGS ABOUT NON-DEFAULT TLD