mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-12-21 03:10:06 +01:00
218 lines
8.3 KiB
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")
|
|
}
|
|
}
|
|
}
|