// // 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)" } }