1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-12-21 03:10:06 +01:00
Files
app/phpmon/Domain/App/AppUpdater.swift

218 lines
8.3 KiB
Swift

//
// AppUpdater.swift
// PHP Monitor
//
// Created by Nico Verbruggen on 04/02/2023.
// Copyright © 2025 Nico Verbruggen. All rights reserved.
//
import Foundation
import Cocoa
import NVAlert
/**
The potential different outcomes of a check for updates.
*/
enum UpdateCheckResult {
case success
case networkError
case parseError
}
/**
Instead of using `UpdateCheck` which is a more simplified update checking process
included in `NVAppUpdater`, we have a slightly more complex setup here.
*/
class AppUpdater {
var caskFile: CaskFile!
var latestVersionOnline: AppVersion!
var interactive: Bool = false
public func checkForUpdates(userInitiated: Bool) async -> UpdateCheckResult {
// If user initiated, we always expect to see an alert
self.interactive = userInitiated
// Log that we're looking for updates
Log.info("The app will search for updates...")
// Attempt to get the latest CaskFile from the API
guard let caskFile = try? await CaskFile.fromUrl(
App.shared.container,
Constants.Urls.UpdateCheckEndpoint
) else {
// ERROR #1: The endpoint is unreachable or the response is invalid.
Log.err("Could not get a valid CaskFile from the endpoint.")
if interactive {
await presentCouldNotRetrieveUpdate()
}
return .networkError
}
// We will now persist the CaskFile so we can reference it later
self.caskFile = caskFile
// Let's parse the latest online version if we can
guard let onlineVersion = AppVersion.from(caskFile.version) else {
// ERROR #2: The CaskFile's version string is invalid.
Log.err("The version string from the CaskFile could not be read.")
if interactive {
await presentCouldNotRetrieveUpdate()
}
return .parseError
}
// We will now persist the version number so we can reference it later
latestVersionOnline = onlineVersion
Log.info("The latest version read from the endpoint is: v\(onlineVersion.computerReadable).")
Task { // Present this concurrently w/ returning the .success value
if latestVersionOnline > AppVersion.fromCurrentVersion() {
await presentNewerVersionAvailableAlert()
} else if interactive {
await presentNoNewerVersionAvailableAlert()
}
}
return .success
}
// MARK: - Alerts
@MainActor public func presentNewerVersionAvailableAlert() {
NVAlert().withInformation(
title: "updater.alerts.newer_version_available.title"
.localized(latestVersionOnline.humanReadable),
subtitle: "updater.alerts.newer_version_available.subtitle"
.localized,
description: BrewDiagnostics.shared.customCaskInstalled
? "updater.installation_source.brew".localized("brew upgrade phpmon")
: "updater.installation_source.direct".localized
)
.withPrimary(
text: "updater.alerts.buttons.install".localized,
action: { vc in
self.cleanupCaskroom()
self.prepareForDownload()
vc.close(with: .OK)
}
)
.withSecondary(
text: "updater.alerts.buttons.release_notes".localized,
action: { _ in
NSWorkspace.shared.open({
if App.identifier.contains(".eap") {
return Constants.Urls.EarlyAccessChangelog
} else {
let urlSegments = self.caskFile.url.split(separator: "/")
let tag = urlSegments[urlSegments.count - 2] // ../download/{tag}/{file.zip}
return Constants.Urls.GitHubReleases.appendingPathComponent("/tag/\(tag)")
}
}())
}
)
.withTertiary(text: "updater.alerts.buttons.dismiss".localized, action: { vc in
vc.close(with: .OK)
})
.show(urgency: interactive ? .bringToFront : .urgentRequestAttention)
}
@MainActor public func presentNoNewerVersionAvailableAlert() {
NVAlert().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(urgency: .bringToFront)
}
@MainActor public func presentCouldNotRetrieveUpdate() {
NVAlert().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(urgency: .bringToFront)
}
// 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"
.replacing("~", with: NSHomeDirectory())
system_quiet("cp -R \"\(updater)\" \"\(updaterDirectory)/PHP Monitor Self-Updater.app\"")
try! App.shared.container.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!")
}
}
private func cleanupCaskroom() {
let path = App.shared.container.paths.caskroomPath
if App.shared.container.filesystem.directoryExists(path) {
Log.info("Removing the Caskroom directory for PHP Monitor...")
do {
try App.shared.container.filesystem.remove(path)
Log.info("Removed the Caskroom directory at `\(path)`.")
} catch {
Log.err("Automatically removing the Caskroom directory at `\(path)` failed.")
}
}
}
// MARK: - Checking if Self-Updater Worked
public static func checkIfUpdateWasPerformed() {
// Cleanup the upgrade.success file
if App.shared.container.filesystem.fileExists("~/.config/phpmon/updater/upgrade.success") {
Task { @MainActor in
if App.identifier.contains(".phpmon.eap") {
LocalNotification.send(
title: "notification.phpmon_updated.title".localized,
subtitle: "notification.phpmon_updated_dev.desc".localized(App.shortVersion, App.bundleVersion),
preference: nil
)
} else {
LocalNotification.send(
title: "notification.phpmon_updated.title".localized,
subtitle: "notification.phpmon_updated.desc".localized(App.shortVersion),
preference: nil
)
}
}
Log.info("The `upgrade.success` file was found! An update was installed. Cleaning up...")
try? App.shared.container.filesystem.remove("~/.config/phpmon/updater/upgrade.success")
}
// Cleanup the previous updater
if App.shared.container.filesystem.anyExists("~/.config/phpmon/updater/PHP Monitor Self-Updater.app") {
Log.info("A remnant of the self-updater must still be removed...")
try? App.shared.container.filesystem.remove("~/.config/phpmon/updater/PHP Monitor Self-Updater.app")
}
}
}