From 0eadd6b26b336b5c5b83c27b6ed0bf068b492336 Mon Sep 17 00:00:00 2001 From: Leopere Date: Tue, 10 Feb 2026 14:03:19 -0500 Subject: [PATCH] bump --- Sources/Pommedoro/AppDelegate.swift | 9 +- Sources/Pommedoro/AutoUpdater.swift | 179 +++------------------------ Sources/Pommedoro/BreakScreens.swift | 2 +- Sources/Pommedoro/StatusBar.swift | 2 +- 4 files changed, 24 insertions(+), 168 deletions(-) diff --git a/Sources/Pommedoro/AppDelegate.swift b/Sources/Pommedoro/AppDelegate.swift index fdfbda8..771883f 100644 --- a/Sources/Pommedoro/AppDelegate.swift +++ b/Sources/Pommedoro/AppDelegate.swift @@ -61,9 +61,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { NotificationManager.shared.requestPermission() NotificationManager.shared.postCycleStarted(durationMinutes: workTimerDuration / 60) - // Check for updates silently on launch - AutoUpdater.checkInBackground(silent: true) - NotificationCenter.default.addObserver( self, selector: #selector(settingsDidChange), name: Settings.didChangeNotification, object: nil @@ -185,8 +182,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { } - @objc func checkForUpdates() { - AutoUpdater.checkInBackground(silent: false) + @objc func updateLocal() { + AutoUpdater.updateLocal(silent: false) } @objc func closePommedoro() { @@ -230,7 +227,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @objc func dismissSuggestion() { currentSuggestionFeedback = .dismissed SuggestionManager.shared.dismiss(suggestion: currentSuggestion) - resumePommedoro() + reflectionDone() } // MARK: - Work Log Helpers diff --git a/Sources/Pommedoro/AutoUpdater.swift b/Sources/Pommedoro/AutoUpdater.swift index c14b77b..4054780 100644 --- a/Sources/Pommedoro/AutoUpdater.swift +++ b/Sources/Pommedoro/AutoUpdater.swift @@ -1,24 +1,12 @@ import Foundation import AppKit -import CommonCrypto -/// Self-update by comparing local state (SHA + timestamp) to remote SHA + Last-Modified -/// from the git server over HTTPS (no login). Update only when server is newer. +/// Update Local: downloads DMG from remote and swaps app. No version or integrity checks. enum AutoUpdater { - private static let devSentinel = "dev" - private static let remoteSHA256URL = URL( - string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg.sha256" - )! private static let remoteDMGURL = URL( string: "https://git.nixc.us/colin/pommedoro/raw/branch/main/releases/Pommedoro.dmg" )! - /// First 64 hex chars (lowercased); tolerates BOM/CRLF/trailing bytes from server. - private static func normalizeSHA(_ s: String) -> String? { - let hex = s.lowercased().filter { "0123456789abcdef".contains($0) } - return hex.count >= 64 ? String(hex.prefix(64)) : nil - } - private static var supportDir: URL { let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! @@ -34,147 +22,36 @@ enum AutoUpdater { private static var stagedAppPath: String { NSTemporaryDirectory() + "Pommedoro-staged.app" } private static var pendingStatePath: String { NSTemporaryDirectory() + "pommedoro-pending-state.txt" } - static func checkInBackground(silent: Bool = true) { - DispatchQueue.global(qos: .utility).async { performCheck(silent: silent) } + static func updateLocal(silent: Bool = false) { + DispatchQueue.global(qos: .utility).async { performUpdateLocal(silent: silent) } } - private static func performCheck(silent: Bool) { - if !silent { showAlert(title: "Check for Updates", message: "Auto-update is disabled.") } - if false { - let local = readLocalState() - if local?.sha == devSentinel { - if !silent { showAlert(title: "Dev Build", message: "Auto-update is disabled for local dev builds.") } - return - } - - guard let remote = fetchRemoteRelease() else { - if !silent { showAlert(title: "Update Check Failed", message: "Could not reach the update server.") } - return - } - - let remoteSHA = Self.normalizeSHA(remote.sha) ?? remote.sha - let localSHA = local?.sha == devSentinel ? local?.sha : (local.flatMap { Self.normalizeSHA($0.sha) }) - if remoteSHA == localSHA { - if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") } - return - } - let localTimestamp = local?.timestamp ?? 0 - if remote.timestamp <= localTimestamp { - if !silent { showAlert(title: "Up to Date", message: "You are running the latest version.") } - return - } - - guard promptForUpdate() else { return } - guard downloadDMG() else { - showAlert(title: "Update Failed", message: "Could not download the update.") - return - } - guard let downloadedSHA = sha256(of: downloadedDMGPath), downloadedSHA == remote.sha else { - showAlert(title: "Update Failed", message: "Downloaded file integrity check failed.") - try? FileManager.default.removeItem(at: downloadedDMGPath) - return - } - guard stageFromDMG() else { - showAlert(title: "Update Failed", message: "Could not extract the update.") - return - } - - try? "\(remote.sha)\n\(Int(remote.timestamp))".write(toFile: pendingStatePath, atomically: true, encoding: .utf8) - spawnSwapAndRelaunch() - - DispatchQueue.main.async { NSApp.terminate(nil) } - } - } - - // MARK: - Fetch from git server (HTTPS, no login) - - private static func fetchRemoteRelease() -> (sha: String, timestamp: TimeInterval)? { + private static func performUpdateLocal(silent: Bool) { + if !silent { showAlert(title: "Update Local", message: "Getting latest version...") } let sem = DispatchSemaphore(value: 0) - var out: (sha: String, timestamp: TimeInterval)? - - let task = URLSession.shared.dataTask(with: remoteSHA256URL) { data, response, _ in - defer { sem.signal() } - guard let data = data, - let http = response as? HTTPURLResponse, - http.statusCode == 200, - let raw = String(data: data, encoding: .utf8), - let sha = Self.normalizeSHA(raw) 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() - sem.wait() - 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 { - let sem = DispatchSemaphore(value: 0) - var ok = false + var downloadOk = false let task = URLSession.shared.downloadTask(with: remoteDMGURL) { url, response, _ in defer { sem.signal() } guard let url = url, (response as? HTTPURLResponse)?.statusCode == 200 else { return } do { try? FileManager.default.removeItem(at: downloadedDMGPath) try FileManager.default.moveItem(at: url, to: downloadedDMGPath) - ok = true - } catch { BugReporting.capture(error) } + downloadOk = true + } catch {} } task.resume() sem.wait() - return ok - } - - private static func sha256(of url: URL) -> String? { - guard let handle = try? FileHandle(forReadingFrom: url) else { return nil } - defer { handle.closeFile() } - var ctx = CC_SHA256_CTX() - CC_SHA256_Init(&ctx) - while autoreleasepool(invoking: { - let chunk = handle.readData(ofLength: 1_048_576) - if chunk.isEmpty { return false } - chunk.withUnsafeBytes { _ = CC_SHA256_Update(&ctx, $0.baseAddress, CC_LONG(chunk.count)) } - return true - }) {} - var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) - CC_SHA256_Final(&digest, &ctx) - return digest.map { String(format: "%02x", $0) }.joined() - } - - // MARK: - Local state (SHA + timestamp, two lines) - - private static func readLocalState() -> (sha: String, timestamp: TimeInterval)? { - guard let raw = try? String(contentsOf: statePath, encoding: .utf8) else { return nil } - let lines = raw.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) - guard let first = lines.first else { return nil } - let trimmed = first.trimmingCharacters(in: .whitespacesAndNewlines) - let sha: String - if trimmed == devSentinel { - sha = devSentinel - } else if let normalized = Self.normalizeSHA(first) { - sha = normalized - } else { - return nil + guard downloadOk else { + if !silent { showAlert(title: "Update Failed", message: "Download error.") } + return } - let ts: TimeInterval = lines.count > 1 ? (TimeInterval(lines[1]) ?? 0) : 0 - return (sha, ts) - } - - static func writeLocalSHA(_ sha: String) { - try? sha.write(to: statePath, atomically: true, encoding: .utf8) + guard stageFromDMG() else { + if !silent { showAlert(title: "Update Failed", message: "Install error.") } + return + } + try? "local\n0".write(toFile: pendingStatePath, atomically: true, encoding: .utf8) + spawnSwapAndRelaunch() + DispatchQueue.main.async { NSApp.terminate(nil) } } private static func stageFromDMG() -> Bool { @@ -189,11 +66,10 @@ enum AutoUpdater { guard FileManager.default.fileExists(atPath: srcApp) else { return false } try? FileManager.default.removeItem(atPath: stagedAppPath) do { try FileManager.default.copyItem(atPath: srcApp, toPath: stagedAppPath) } - catch { BugReporting.capture(error); return false } + catch { return false } return true } - /// Script: wait for process exit, swap app, write state from pending file, relaunch. private static func spawnSwapAndRelaunch() { let pid = ProcessInfo.processInfo.processIdentifier let staged = stagedAppPath @@ -236,23 +112,6 @@ enum AutoUpdater { return proc.terminationStatus == 0 } - private static func promptForUpdate() -> Bool { - var result = false - let sem = DispatchSemaphore(value: 0) - DispatchQueue.main.async { - let alert = NSAlert() - alert.messageText = "Update Available" - alert.informativeText = "A new version of Pommedoro is available. Update now?" - alert.alertStyle = .informational - alert.addButton(withTitle: "Update") - alert.addButton(withTitle: "Later") - result = alert.runModal() == .alertFirstButtonReturn - sem.signal() - } - sem.wait() - return result - } - private static func showAlert(title: String, message: String) { DispatchQueue.main.async { let a = NSAlert() diff --git a/Sources/Pommedoro/BreakScreens.swift b/Sources/Pommedoro/BreakScreens.swift index 920fecd..2b096d3 100644 --- a/Sources/Pommedoro/BreakScreens.swift +++ b/Sources/Pommedoro/BreakScreens.swift @@ -144,7 +144,7 @@ extension AppDelegate { @objc func skipSuggestion() { currentSuggestionFeedback = .skipped - resumePommedoro() + reflectionDone() } @objc func didSuccess() { diff --git a/Sources/Pommedoro/StatusBar.swift b/Sources/Pommedoro/StatusBar.swift index ed707d3..6aeaa1e 100644 --- a/Sources/Pommedoro/StatusBar.swift +++ b/Sources/Pommedoro/StatusBar.swift @@ -36,7 +36,7 @@ extension AppDelegate { launchAtLoginMenuItem.state = LaunchAgent.isInstalled() ? .on : .off menu.addItem(launchAtLoginMenuItem) menu.addItem(NSMenuItem.separator()) - menu.addItem(NSMenuItem(title: "Check for Updates…", action: #selector(checkForUpdates), keyEquivalent: "u")) + menu.addItem(NSMenuItem(title: "Update Local", action: #selector(updateLocal), keyEquivalent: "u")) menu.addItem(NSMenuItem(title: "Quit Pommedoro", action: #selector(closePommedoro), keyEquivalent: "q")) for item in menu.items {