Fix update logic: SHA+timestamp from git over HTTPS, persist after swap
- Fetch SHA and Last-Modified from repo over HTTPS (no login); use timestamp so 'update' only when server is newer, not just different. - Local state: two-line file (SHA + Unix timestamp); script writes state after successful swap so it stays correct. - install.command: fetch remote via HTTPS, stamp SHA+timestamp (or 0 if install not matching remote). - build-test: verify first line of state file matches release SHA. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7eac0ad717
commit
65114fc265
|
|
@ -8,6 +8,7 @@ APP_NAME="Pommedoro"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
APP_SRC="${SCRIPT_DIR}/${APP_NAME}.app"
|
APP_SRC="${SCRIPT_DIR}/${APP_NAME}.app"
|
||||||
APP_DST="/Applications/${APP_NAME}.app"
|
APP_DST="/Applications/${APP_NAME}.app"
|
||||||
|
REMOTE_SHA_URL="https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo " Installing ${APP_NAME}..."
|
echo " Installing ${APP_NAME}..."
|
||||||
|
|
@ -32,13 +33,29 @@ cp -R "${APP_SRC}" "${APP_DST}"
|
||||||
# Strip quarantine attribute (fixes "app is damaged" on downloaded DMGs)
|
# Strip quarantine attribute (fixes "app is damaged" on downloaded DMGs)
|
||||||
xattr -cr "${APP_DST}"
|
xattr -cr "${APP_DST}"
|
||||||
|
|
||||||
# Stamp SHA256 so auto-updater knows current version
|
# Stamp state (SHA + timestamp) for auto-updater via HTTPS, no login
|
||||||
SHA_DIR="${HOME}/Library/Application Support/Pommedoro"
|
SHA_DIR="${HOME}/Library/Application Support/Pommedoro"
|
||||||
mkdir -p "${SHA_DIR}"
|
mkdir -p "${SHA_DIR}"
|
||||||
# Find the source .dmg backing this mounted volume
|
OUR_SHA=""
|
||||||
DMG_DEVICE="$(hdiutil info 2>/dev/null | grep -B 20 "${SCRIPT_DIR}" | grep 'image-path' | awk -F' : ' '{print $2}' | head -1)"
|
DMG_DEVICE="$(hdiutil info 2>/dev/null | grep -B 20 "${SCRIPT_DIR}" | grep 'image-path' | awk -F' : ' '{print $2}' | head -1)"
|
||||||
if [ -n "${DMG_DEVICE}" ] && [ -f "${DMG_DEVICE}" ]; then
|
if [ -n "${DMG_DEVICE}" ] && [ -f "${DMG_DEVICE}" ]; then
|
||||||
shasum -a 256 "${DMG_DEVICE}" | awk '{print $1}' > "${SHA_DIR}/current.sha256"
|
OUR_SHA="$(shasum -a 256 "${DMG_DEVICE}" | awk '{print $1}')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOTE_SHA=""
|
||||||
|
REMOTE_TS="0"
|
||||||
|
if [ -n "${OUR_SHA}" ]; then
|
||||||
|
REMOTE_SHA="$(curl -sL "${REMOTE_SHA_URL}" 2>/dev/null | head -1 | tr -d '\r')"
|
||||||
|
LAST_MOD="$(curl -sIL "${REMOTE_SHA_URL}" 2>/dev/null | grep -i '^Last-Modified:' | head -1 | sed 's/^Last-Modified: *//' | tr -d '\r')"
|
||||||
|
if [ -n "${LAST_MOD}" ]; then
|
||||||
|
REMOTE_TS="$(date -j -f "%a, %d %b %Y %H:%M:%S %Z" "${LAST_MOD}" "+%s" 2>/dev/null || echo "0")"
|
||||||
|
fi
|
||||||
|
# If this DMG matches remote, use remote timestamp; else use 0 so app may prompt later
|
||||||
|
if [ "${OUR_SHA}" = "${REMOTE_SHA}" ] && [ -n "${REMOTE_SHA}" ]; then
|
||||||
|
printf "%s\n%s\n" "${OUR_SHA}" "${REMOTE_TS}" > "${SHA_DIR}/current.sha256"
|
||||||
|
else
|
||||||
|
printf "%s\n0\n" "${OUR_SHA}" > "${SHA_DIR}/current.sha256"
|
||||||
|
fi
|
||||||
echo " SHA256 stamped for auto-update."
|
echo " SHA256 stamped for auto-update."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,253 +2,200 @@ import Foundation
|
||||||
import AppKit
|
import AppKit
|
||||||
import CommonCrypto
|
import CommonCrypto
|
||||||
|
|
||||||
/// Handles automatic self-update by comparing the local DMG SHA256 against
|
/// Self-update by comparing local state (SHA + timestamp) to remote SHA + Last-Modified
|
||||||
/// the one published in the repository. When a mismatch is detected the new
|
/// from the git server over HTTPS (no login). Update only when server is newer.
|
||||||
/// DMG is downloaded, mounted, and the app is reinstalled + relaunched.
|
|
||||||
enum AutoUpdater {
|
enum AutoUpdater {
|
||||||
|
|
||||||
// MARK: - Configuration
|
|
||||||
|
|
||||||
/// Sentinel value written by `make install` to indicate a dev build.
|
|
||||||
/// The updater skips automatic updates when this is the local stamp.
|
|
||||||
private static let devSentinel = "dev"
|
private static let devSentinel = "dev"
|
||||||
|
|
||||||
/// Raw URL for the SHA256 file in the repo (Gitea raw endpoint)
|
|
||||||
private static let remoteSHA256URL = URL(
|
private static let remoteSHA256URL = URL(
|
||||||
string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256"
|
string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
/// Raw URL for the DMG itself
|
|
||||||
private static let remoteDMGURL = URL(
|
private static let remoteDMGURL = URL(
|
||||||
string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg"
|
string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg"
|
||||||
)!
|
)!
|
||||||
|
|
||||||
/// Where to store the local copy of the last-known SHA256
|
private static var supportDir: URL {
|
||||||
private static var localSHAPath: URL {
|
|
||||||
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
||||||
let dir = support.appendingPathComponent("Pommedoro", isDirectory: true)
|
let dir = support.appendingPathComponent("Pommedoro", isDirectory: true)
|
||||||
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
||||||
return dir.appendingPathComponent("current.sha256")
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Temporary download location for the DMG
|
private static var statePath: URL { supportDir.appendingPathComponent("current.sha256") }
|
||||||
private static var downloadedDMGPath: URL {
|
private static var downloadedDMGPath: URL {
|
||||||
FileManager.default.temporaryDirectory.appendingPathComponent("Pommedoro-update.dmg")
|
FileManager.default.temporaryDirectory.appendingPathComponent("Pommedoro-update.dmg")
|
||||||
}
|
}
|
||||||
|
private static var stagedAppPath: String { NSTemporaryDirectory() + "Pommedoro-staged.app" }
|
||||||
|
private static var pendingStatePath: String { NSTemporaryDirectory() + "pommedoro-pending-state.txt" }
|
||||||
|
|
||||||
/// Staging location for the new .app before swap
|
|
||||||
private static var stagedAppPath: String {
|
|
||||||
NSTemporaryDirectory() + "Pommedoro-staged.app"
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Public API
|
|
||||||
|
|
||||||
/// Check for updates in the background. Optionally show UI feedback.
|
|
||||||
static func checkInBackground(silent: Bool = true) {
|
static func checkInBackground(silent: Bool = true) {
|
||||||
DispatchQueue.global(qos: .utility).async {
|
DispatchQueue.global(qos: .utility).async { performCheck(silent: silent) }
|
||||||
self.performCheck(silent: silent)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Core Logic
|
|
||||||
|
|
||||||
private static func performCheck(silent: Bool) {
|
private static func performCheck(silent: Bool) {
|
||||||
// 1. Read local SHA256 (written at last install/build)
|
let local = readLocalState()
|
||||||
let localSHA = readLocalSHA()
|
if local?.sha == devSentinel {
|
||||||
|
if !silent { showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.") }
|
||||||
// 2. Dev builds are stamped "dev" – skip silent checks entirely
|
|
||||||
if localSHA == devSentinel {
|
|
||||||
if !silent {
|
|
||||||
showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.")
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Fetch remote SHA256
|
guard let remote = fetchRemoteRelease() else {
|
||||||
guard let remoteSHA = fetchRemoteSHA() else {
|
|
||||||
if !silent { showAlert(title: "Update Check Failed", message: "Could not reach the update server.") }
|
if !silent { showAlert(title: "Update Check Failed", message: "Could not reach the update server.") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Compare
|
let localTimestamp = local?.timestamp ?? 0
|
||||||
if remoteSHA == localSHA {
|
if remote.timestamp <= localTimestamp {
|
||||||
|
if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if remote.sha == local?.sha {
|
||||||
if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") }
|
if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. There's an update – prompt user
|
guard promptForUpdate() else { return }
|
||||||
let shouldUpdate = promptForUpdate()
|
|
||||||
guard shouldUpdate else { return }
|
|
||||||
|
|
||||||
// 6. Download new DMG
|
|
||||||
guard downloadDMG() else {
|
guard downloadDMG() else {
|
||||||
showAlert(title: "Update Failed", message: "Could not download the update.")
|
showAlert(title: "Update Failed", message: "Could not download the update.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
guard let downloadedSHA = sha256(of: downloadedDMGPath), downloadedSHA == remote.sha else {
|
||||||
// 7. Verify downloaded DMG matches remote SHA
|
|
||||||
guard let downloadedSHA = sha256(of: downloadedDMGPath), downloadedSHA == remoteSHA else {
|
|
||||||
showAlert(title: "Update Failed", message: "Downloaded file integrity check failed.")
|
showAlert(title: "Update Failed", message: "Downloaded file integrity check failed.")
|
||||||
try? FileManager.default.removeItem(at: downloadedDMGPath)
|
try? FileManager.default.removeItem(at: downloadedDMGPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 8. Extract new app from DMG to staging area
|
|
||||||
guard stageFromDMG() else {
|
guard stageFromDMG() else {
|
||||||
showAlert(title: "Update Failed", message: "Could not extract the update.")
|
showAlert(title: "Update Failed", message: "Could not extract the update.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Save new SHA so we don't re-update next launch
|
try? "\(remote.sha)\n\(Int(remote.timestamp))".write(toFile: pendingStatePath, atomically: true, encoding: .utf8)
|
||||||
try? remoteSHA.write(to: localSHAPath, atomically: true, encoding: .utf8)
|
|
||||||
|
|
||||||
// 10. Spawn swap script, then terminate so applicationWillTerminate saves state
|
|
||||||
spawnSwapAndRelaunch()
|
spawnSwapAndRelaunch()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { NSApp.terminate(nil) }
|
||||||
NSApp.terminate(nil)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Networking
|
// MARK: - Fetch from git server (HTTPS, no login)
|
||||||
|
|
||||||
private static func fetchRemoteSHA() -> String? {
|
private static func fetchRemoteRelease() -> (sha: String, timestamp: TimeInterval)? {
|
||||||
let sem = DispatchSemaphore(value: 0)
|
let sem = DispatchSemaphore(value: 0)
|
||||||
var result: String?
|
var out: (sha: String, timestamp: TimeInterval)?
|
||||||
|
|
||||||
let task = URLSession.shared.dataTask(with: remoteSHA256URL) { data, response, _ in
|
let task = URLSession.shared.dataTask(with: remoteSHA256URL) { data, response, _ in
|
||||||
defer { sem.signal() }
|
defer { sem.signal() }
|
||||||
guard let data = data,
|
guard let data = data,
|
||||||
let http = response as? HTTPURLResponse,
|
let http = response as? HTTPURLResponse,
|
||||||
http.statusCode == 200,
|
http.statusCode == 200,
|
||||||
let str = String(data: data, encoding: .utf8) else { return }
|
let sha = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
result = str.trimmingCharacters(in: .whitespacesAndNewlines)
|
!sha.isEmpty else { return }
|
||||||
|
let ts: TimeInterval
|
||||||
|
if let lastMod = http.value(forHTTPHeaderField: "Last-Modified"),
|
||||||
|
let date = Self.parseHTTPDate(lastMod) {
|
||||||
|
ts = date.timeIntervalSince1970
|
||||||
|
} else {
|
||||||
|
ts = 0
|
||||||
|
}
|
||||||
|
out = (sha, ts)
|
||||||
}
|
}
|
||||||
task.resume()
|
task.resume()
|
||||||
sem.wait()
|
sem.wait()
|
||||||
return result
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func parseHTTPDate(_ s: String) -> Date? {
|
||||||
|
let fmt = DateFormatter()
|
||||||
|
fmt.locale = Locale(identifier: "en_US_POSIX")
|
||||||
|
fmt.timeZone = TimeZone(identifier: "GMT")
|
||||||
|
fmt.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
|
||||||
|
return fmt.date(from: s)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func downloadDMG() -> Bool {
|
private static func downloadDMG() -> Bool {
|
||||||
let sem = DispatchSemaphore(value: 0)
|
let sem = DispatchSemaphore(value: 0)
|
||||||
var success = false
|
var ok = false
|
||||||
|
|
||||||
let task = URLSession.shared.downloadTask(with: remoteDMGURL) { url, response, _ in
|
let task = URLSession.shared.downloadTask(with: remoteDMGURL) { url, response, _ in
|
||||||
defer { sem.signal() }
|
defer { sem.signal() }
|
||||||
guard let url = url,
|
guard let url = url, (response as? HTTPURLResponse)?.statusCode == 200 else { return }
|
||||||
let http = response as? HTTPURLResponse,
|
|
||||||
http.statusCode == 200 else { return }
|
|
||||||
do {
|
do {
|
||||||
let dest = downloadedDMGPath
|
try? FileManager.default.removeItem(at: downloadedDMGPath)
|
||||||
try? FileManager.default.removeItem(at: dest)
|
try FileManager.default.moveItem(at: url, to: downloadedDMGPath)
|
||||||
try FileManager.default.moveItem(at: url, to: dest)
|
ok = true
|
||||||
success = true
|
} catch { BugReporting.capture(error) }
|
||||||
} catch {
|
|
||||||
BugReporting.capture(error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
task.resume()
|
task.resume()
|
||||||
sem.wait()
|
sem.wait()
|
||||||
return success
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - SHA256 Computation
|
private static func sha256(of url: URL) -> String? {
|
||||||
|
guard let handle = try? FileHandle(forReadingFrom: url) else { return nil }
|
||||||
static func sha256(of fileURL: URL) -> String? {
|
|
||||||
guard let handle = try? FileHandle(forReadingFrom: fileURL) else { return nil }
|
|
||||||
defer { handle.closeFile() }
|
defer { handle.closeFile() }
|
||||||
|
|
||||||
var ctx = CC_SHA256_CTX()
|
var ctx = CC_SHA256_CTX()
|
||||||
CC_SHA256_Init(&ctx)
|
CC_SHA256_Init(&ctx)
|
||||||
|
|
||||||
while autoreleasepool(invoking: {
|
while autoreleasepool(invoking: {
|
||||||
let chunk = handle.readData(ofLength: 1_048_576) // 1 MB
|
let chunk = handle.readData(ofLength: 1_048_576)
|
||||||
if chunk.isEmpty { return false }
|
if chunk.isEmpty { return false }
|
||||||
chunk.withUnsafeBytes { ptr in
|
chunk.withUnsafeBytes { _ = CC_SHA256_Update(&ctx, $0.baseAddress, CC_LONG(chunk.count)) }
|
||||||
_ = CC_SHA256_Update(&ctx, ptr.baseAddress, CC_LONG(chunk.count))
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
}) {}
|
}) {}
|
||||||
|
|
||||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||||
CC_SHA256_Final(&digest, &ctx)
|
CC_SHA256_Final(&digest, &ctx)
|
||||||
return digest.map { String(format: "%02x", $0) }.joined()
|
return digest.map { String(format: "%02x", $0) }.joined()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Local SHA
|
// MARK: - Local state (SHA + timestamp, two lines)
|
||||||
|
|
||||||
private static func readLocalSHA() -> String? {
|
private static func readLocalState() -> (sha: String, timestamp: TimeInterval)? {
|
||||||
guard let str = try? String(contentsOf: localSHAPath, encoding: .utf8) else { return nil }
|
guard let raw = try? String(contentsOf: statePath, encoding: .utf8) else { return nil }
|
||||||
return str.trimmingCharacters(in: .whitespacesAndNewlines)
|
let lines = raw.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
|
||||||
|
guard let sha = lines.first?.trimmingCharacters(in: .whitespacesAndNewlines), !sha.isEmpty else { return nil }
|
||||||
|
let ts: TimeInterval = lines.count > 1 ? (TimeInterval(lines[1]) ?? 0) : 0
|
||||||
|
return (sha, ts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write the current SHA256 so subsequent update checks know the baseline.
|
|
||||||
static func writeLocalSHA(_ sha: String) {
|
static func writeLocalSHA(_ sha: String) {
|
||||||
try? sha.write(to: localSHAPath, atomically: true, encoding: .utf8)
|
try? sha.write(to: statePath, atomically: true, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Stage from DMG
|
|
||||||
|
|
||||||
/// Mount the downloaded DMG and copy the .app to a temp staging directory.
|
|
||||||
/// This avoids replacing the running binary in-place.
|
|
||||||
private static func stageFromDMG() -> Bool {
|
private static func stageFromDMG() -> Bool {
|
||||||
let dmg = downloadedDMGPath.path
|
let dmg = downloadedDMGPath.path
|
||||||
let mountPoint = "/tmp/pommedoro-update-mount"
|
let mountPoint = "/tmp/pommedoro-update-mount"
|
||||||
|
guard runShell("hdiutil attach \"\(dmg)\" -mountpoint \"\(mountPoint)\" -nobrowse -quiet") else { return false }
|
||||||
let mountOK = runShell("hdiutil attach \"\(dmg)\" -mountpoint \"\(mountPoint)\" -nobrowse -quiet")
|
|
||||||
guard mountOK else { return false }
|
|
||||||
|
|
||||||
defer {
|
defer {
|
||||||
_ = runShell("hdiutil detach \"\(mountPoint)\" -quiet")
|
_ = runShell("hdiutil detach \"\(mountPoint)\" -quiet")
|
||||||
try? FileManager.default.removeItem(at: downloadedDMGPath)
|
try? FileManager.default.removeItem(at: downloadedDMGPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
let srcApp = "\(mountPoint)/Pommedoro.app"
|
let srcApp = "\(mountPoint)/Pommedoro.app"
|
||||||
guard FileManager.default.fileExists(atPath: srcApp) else { return false }
|
guard FileManager.default.fileExists(atPath: srcApp) else { return false }
|
||||||
|
|
||||||
// Copy to staging
|
|
||||||
try? FileManager.default.removeItem(atPath: stagedAppPath)
|
try? FileManager.default.removeItem(atPath: stagedAppPath)
|
||||||
do {
|
do { try FileManager.default.copyItem(atPath: srcApp, toPath: stagedAppPath) }
|
||||||
try FileManager.default.copyItem(atPath: srcApp, toPath: stagedAppPath)
|
catch { BugReporting.capture(error); return false }
|
||||||
} catch {
|
|
||||||
BugReporting.capture(error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Swap and Relaunch
|
/// Script: wait for process exit, swap app, write state from pending file, relaunch.
|
||||||
|
|
||||||
/// Spawns a background shell script that:
|
|
||||||
/// 1. Waits for the current process to exit
|
|
||||||
/// 2. Replaces /Applications/Pommedoro.app with the staged copy
|
|
||||||
/// 3. Strips quarantine
|
|
||||||
/// 4. Relaunches the app
|
|
||||||
/// The script is fire-and-forget so the current process can terminate cleanly.
|
|
||||||
private static func spawnSwapAndRelaunch() {
|
private static func spawnSwapAndRelaunch() {
|
||||||
let pid = ProcessInfo.processInfo.processIdentifier
|
let pid = ProcessInfo.processInfo.processIdentifier
|
||||||
let staged = stagedAppPath
|
let staged = stagedAppPath
|
||||||
let dest = "/Applications/Pommedoro.app"
|
let dest = "/Applications/Pommedoro.app"
|
||||||
|
let stateFile = statePath.path
|
||||||
|
let pending = pendingStatePath
|
||||||
|
|
||||||
let script = """
|
let script = """
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Wait for the old process to exit (up to 10 seconds)
|
for i in $(seq 1 20); do kill -0 \(pid) 2>/dev/null || break; sleep 0.5; done
|
||||||
for i in $(seq 1 20); do
|
|
||||||
kill -0 \(pid) 2>/dev/null || break
|
|
||||||
sleep 0.5
|
|
||||||
done
|
|
||||||
# Swap
|
|
||||||
rm -rf "\(dest)"
|
rm -rf "\(dest)"
|
||||||
mv "\(staged)" "\(dest)"
|
mv "\(staged)" "\(dest)"
|
||||||
xattr -cr "\(dest)"
|
xattr -cr "\(dest)"
|
||||||
# Relaunch
|
if [ -f "\(pending)" ]; then
|
||||||
|
mkdir -p "$(dirname "\(stateFile)")"
|
||||||
|
cp "\(pending)" "\(stateFile)"
|
||||||
|
rm -f "\(pending)"
|
||||||
|
fi
|
||||||
open "\(dest)"
|
open "\(dest)"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
let scriptPath = NSTemporaryDirectory() + "pommedoro-swap.sh"
|
let scriptPath = NSTemporaryDirectory() + "pommedoro-swap.sh"
|
||||||
try? script.write(toFile: scriptPath, atomically: true, encoding: .utf8)
|
try? script.write(toFile: scriptPath, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
let proc = Process()
|
let proc = Process()
|
||||||
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||||
proc.arguments = [scriptPath]
|
proc.arguments = [scriptPath]
|
||||||
|
|
@ -258,25 +205,16 @@ enum AutoUpdater {
|
||||||
try? proc.run()
|
try? proc.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Shell Helper
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
private static func runShell(_ command: String) -> Bool {
|
private static func runShell(_ command: String) -> Bool {
|
||||||
let proc = Process()
|
let proc = Process()
|
||||||
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
proc.executableURL = URL(fileURLWithPath: "/bin/bash")
|
||||||
proc.arguments = ["-c", command]
|
proc.arguments = ["-c", command]
|
||||||
proc.standardOutput = FileHandle.nullDevice
|
proc.standardOutput = FileHandle.nullDevice
|
||||||
proc.standardError = FileHandle.nullDevice
|
proc.standardError = FileHandle.nullDevice
|
||||||
do {
|
guard (try? proc.run()) != nil else { return false }
|
||||||
try proc.run()
|
|
||||||
proc.waitUntilExit()
|
proc.waitUntilExit()
|
||||||
return proc.terminationStatus == 0
|
return proc.terminationStatus == 0
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - User Prompts
|
|
||||||
|
|
||||||
private static func promptForUpdate() -> Bool {
|
private static func promptForUpdate() -> Bool {
|
||||||
var result = false
|
var result = false
|
||||||
|
|
@ -284,7 +222,7 @@ enum AutoUpdater {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
alert.messageText = "Update Available"
|
alert.messageText = "Update Available"
|
||||||
alert.informativeText = "A new version of Pommedoro is available. Would you like to update now?"
|
alert.informativeText = "A new version of Pommedoro is available. Update now?"
|
||||||
alert.alertStyle = .informational
|
alert.alertStyle = .informational
|
||||||
alert.addButton(withTitle: "Update")
|
alert.addButton(withTitle: "Update")
|
||||||
alert.addButton(withTitle: "Later")
|
alert.addButton(withTitle: "Later")
|
||||||
|
|
@ -297,12 +235,12 @@ enum AutoUpdater {
|
||||||
|
|
||||||
private static func showAlert(title: String, message: String) {
|
private static func showAlert(title: String, message: String) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let alert = NSAlert()
|
let a = NSAlert()
|
||||||
alert.messageText = title
|
a.messageText = title
|
||||||
alert.informativeText = message
|
a.informativeText = message
|
||||||
alert.alertStyle = .informational
|
a.alertStyle = .informational
|
||||||
alert.addButton(withTitle: "OK")
|
a.addButton(withTitle: "OK")
|
||||||
alert.runModal()
|
a.runModal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,15 +62,21 @@ else
|
||||||
echo "==> OK: no quarantine attribute."
|
echo "==> OK: no quarantine attribute."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> Verifying SHA256 was stamped locally..."
|
echo "==> Verifying state (SHA + timestamp) was stamped locally..."
|
||||||
SHA_FILE="${HOME}/Library/Application Support/Pommedoro/current.sha256"
|
SHA_FILE="${HOME}/Library/Application Support/Pommedoro/current.sha256"
|
||||||
if [ ! -f "${SHA_FILE}" ]; then
|
if [ ! -f "${SHA_FILE}" ]; then
|
||||||
echo "==> FAIL: local SHA256 stamp not found at ${SHA_FILE}."
|
echo "==> FAIL: local state file not found at ${SHA_FILE}."
|
||||||
hdiutil detach /Volumes/Pommedoro -quiet 2>/dev/null || true
|
hdiutil detach /Volumes/Pommedoro -quiet 2>/dev/null || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
LOCAL_SHA="$(cat "${SHA_FILE}")"
|
LOCAL_SHA="$(head -1 "${SHA_FILE}")"
|
||||||
echo "==> OK: local SHA256 stamp = ${LOCAL_SHA}"
|
EXPECTED_SHA="$(cat releases/Pommedoro.dmg.sha256)"
|
||||||
|
if [ "${LOCAL_SHA}" != "${EXPECTED_SHA}" ]; then
|
||||||
|
echo "==> FAIL: local SHA ${LOCAL_SHA} != expected ${EXPECTED_SHA}."
|
||||||
|
hdiutil detach /Volumes/Pommedoro -quiet 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> OK: local state SHA = ${LOCAL_SHA}"
|
||||||
|
|
||||||
hdiutil detach /Volumes/Pommedoro -quiet 2>/dev/null || true
|
hdiutil detach /Volumes/Pommedoro -quiet 2>/dev/null || true
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1 +1 @@
|
||||||
1990cd4168e219d731b10e7e61b374a671666f5486875b4f839a133f29eed068
|
1030ff4f33acbe9694bb9260bde5b1871e9ad42af3c7813f7ac99164f2335c2e
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue