mirror of
https://github.com/nicoverbruggen/phpmon.git
synced 2025-08-06 11:30:08 +02:00
163 lines
6.2 KiB
Swift
163 lines
6.2 KiB
Swift
//
|
|
// Updater.swift
|
|
// PHP Monitor Updater
|
|
//
|
|
// Created by Nico Verbruggen on 01/02/2023.
|
|
// Copyright © 2023 Nico Verbruggen. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
class Updater: NSObject, NSApplicationDelegate {
|
|
|
|
var updaterDirectory: String = ""
|
|
var manifestPath: String = ""
|
|
var manifest: ReleaseManifest! = nil
|
|
|
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
Task { await self.installUpdate() }
|
|
}
|
|
|
|
func installUpdate() async {
|
|
print("PHP MONITOR SELF-UPDATER by Nico Verbruggen")
|
|
print("===========================================")
|
|
|
|
self.updaterDirectory = "~/.config/phpmon/updater"
|
|
.replacingOccurrences(of: "~", with: NSHomeDirectory())
|
|
|
|
print("Updater directory set to: \(self.updaterDirectory)")
|
|
|
|
self.manifestPath = "\(updaterDirectory)/update.json"
|
|
|
|
// Fetch the manifest on the local filesystem
|
|
let manifest = await parseManifest()!
|
|
|
|
// Download the latest file
|
|
let zipPath = await download(manifest)
|
|
|
|
// Terminate all instances of PHP Monitor first
|
|
await LaunchControl.terminateApplications(bundleIds: [
|
|
"com.nicoverbruggen.phpmon.eap",
|
|
"com.nicoverbruggen.phpmon.dev",
|
|
"com.nicoverbruggen.phpmon"
|
|
])
|
|
|
|
// Install the app based on the zip
|
|
let appPath = await extractAndInstall(zipPath: zipPath)
|
|
|
|
// Restart PHP Monitor, this will also close the updater
|
|
_ = await LaunchControl.startApplication(at: appPath)
|
|
|
|
exit(1)
|
|
}
|
|
|
|
func applicationWillTerminate(_ aNotification: Notification) {
|
|
exit(1)
|
|
}
|
|
|
|
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
|
|
return false
|
|
}
|
|
|
|
private func parseManifest() async -> ReleaseManifest? {
|
|
// Read out the correct information from the manifest JSON
|
|
print("Checking manifest file at \(manifestPath)...")
|
|
|
|
do {
|
|
let manifestText = try String(contentsOfFile: manifestPath)
|
|
manifest = try JSONDecoder().decode(ReleaseManifest.self, from: manifestText.data(using: .utf8)!)
|
|
return manifest
|
|
} catch {
|
|
print("Parsing the manifest failed (or the manifest file doesn't exist)!")
|
|
await Alert.show(description: "The manifest file for a potential update was not found. Please try searching for updates again in PHP Monitor.")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func download(_ manifest: ReleaseManifest) async -> String {
|
|
// Remove all zips
|
|
system_quiet("rm -rf \(updaterDirectory)/*.zip")
|
|
|
|
// Download the file (and follow redirects + no output on failure)
|
|
system_quiet("cd \"\(updaterDirectory)\" && curl \(manifest.url) -fLO --max-time 20")
|
|
|
|
// Identify the downloaded file
|
|
let filename = system("cd \"\(updaterDirectory)\" && ls | grep .zip")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
// Ensure the zip exists
|
|
if filename.isEmpty {
|
|
print("The update has not been downloaded. Sadly, that means that PHP Monitor cannot not updated!")
|
|
await Alert.show(description: "The update could not be downloaded, or the file was not correctly written to disk. \n\nPlease try again. \n\n(Note that the download will time-out after 20 seconds, so for slow connections it is recommended to manually download the update.)")
|
|
}
|
|
|
|
// Calculate the checksum for the downloaded file
|
|
let checksum = system("openssl dgst -sha256 \"\(updaterDirectory)/\(filename)\" | awk '{print $NF}'")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
// Compare the checksums
|
|
print("""
|
|
Comparing checksums...
|
|
Expected SHA256: \(manifest.sha256)
|
|
Actual SHA256: \(checksum)
|
|
""")
|
|
|
|
// Make sure the checksum matches before we do anything with the file
|
|
if checksum != manifest.sha256 {
|
|
print("The checksums failed to match. Cancelling!")
|
|
await Alert.show(description: "The downloaded update failed checksum validation. Please try again. If this issue persists, there may be an issue with the server and I do not recommend upgrading.")
|
|
}
|
|
|
|
// Return the path to the zip
|
|
return "\(updaterDirectory)/\(filename)"
|
|
}
|
|
|
|
private func extractAndInstall(zipPath: String) async -> String {
|
|
// Remove the directory that will contain the extracted update
|
|
system_quiet("rm -rf \"\(updaterDirectory)/extracted\"")
|
|
|
|
// Recreate the directory where we will unzip the .app file
|
|
system_quiet("mkdir -p \"\(updaterDirectory)/extracted\"")
|
|
|
|
// Make sure the updater directory exists
|
|
var isDirectory: ObjCBool = true
|
|
if !FileManager.default.fileExists(atPath: "\(updaterDirectory)/extracted", isDirectory: &isDirectory) {
|
|
await Alert.show(description: "The updater directory is missing. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
|
}
|
|
|
|
// Unzip the file
|
|
system_quiet("unzip \"\(zipPath)\" -d \"\(updaterDirectory)/extracted\"")
|
|
|
|
// Find the .app file
|
|
let app = system("ls \"\(updaterDirectory)/extracted\" | grep .app")
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
|
|
print("Finished extracting: \(updaterDirectory)/extracted/\(app)")
|
|
|
|
// Make sure the file was extracted
|
|
if app.isEmpty {
|
|
await Alert.show(description: "The downloaded file could not be extracted. The automatic updater will quit. Make sure that ` ~/.config/phpmon/updater` is writeable.")
|
|
}
|
|
|
|
// Remove the original app
|
|
print("Removing \(app) before replacing...")
|
|
system_quiet("rm -rf \"/Applications/\(app)\"")
|
|
|
|
// Move the new app in place
|
|
system_quiet("mv \"\(updaterDirectory)/extracted/\(app)\" \"/Applications/\(app)\"")
|
|
|
|
// Remove the zip
|
|
system_quiet("rm \"\(zipPath)\"")
|
|
|
|
// Remove the manifest
|
|
system_quiet("rm \"\(manifestPath)\"")
|
|
|
|
// Write a file that is only written when we upgraded successfully
|
|
system_quiet("touch \"\(updaterDirectory)/upgrade.success\"")
|
|
|
|
// Return the new location of the app
|
|
return "/Applications/\(app)"
|
|
}
|
|
}
|