XPC updater POC

This commit is contained in:
Max Goedjen
2025-09-05 19:20:53 -07:00
parent 558ae15b2d
commit 029a4939fa
8 changed files with 385 additions and 25 deletions

View File

@@ -1,5 +1,9 @@
import Foundation
@objc public protocol ReleaseProtocol: Sendable {
}
/// A release is a representation of a downloadable update.
public struct Release: Codable, Sendable {

View File

@@ -0,0 +1,27 @@
import Foundation
@objc public protocol ReleasesDownloaderProtocol {
func downloadReleases(with reply: @escaping (Data?, (any Error)?) -> Void)
}
extension ReleasesDownloaderProtocol {
func downloadReleases() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
downloadReleases { data, error in
if let error {
continuation.resume(throwing: error)
} else if let data {
continuation.resume(returning: data)
} else {
continuation.resume(throwing: NoDataError())
}
}
}
}
}
struct NoDataError: Error {}

View File

@@ -33,27 +33,36 @@ import Observation
) {
self.osVersion = osVersion
self.currentVersion = currentVersion
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
Task {
await checkForUpdates()
}
}
Task {
if checkOnLaunch {
try await checkForUpdates()
}
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(Int(checkFrequency)))
await checkForUpdates()
try await checkForUpdates()
}
}
}
/// Manually trigger an update check.
public func checkForUpdates() async {
guard let (data, _) = try? await URLSession.shared.data(from: Constants.updateURL) else { return }
guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
public func checkForUpdates() async throws {
let releaseData = try await withXPCCall(to: "com.maxgoedjen.Secretive.ReleasesDownloader", ReleasesDownloaderProtocol.self) {
try await $0.downloadReleases()
}
let releases = try JSONDecoder().decode([Release].self, from: releaseData)
await evaluate(releases: releases)
}
func withXPCCall<ServiceProtocol, Result>(to service: String, _: ServiceProtocol.Type, closure: (ServiceProtocol) async throws -> Result) async rethrows -> Result {
let connectionToService = NSXPCConnection(serviceName: "com.maxgoedjen.Secretive.ReleasesDownloader")
connectionToService.remoteObjectInterface = NSXPCInterface(with: (any ReleasesDownloaderProtocol).self)// fixme
connectionToService.resume()
let service = connectionToService.remoteObjectProxy as! ServiceProtocol
let result = try await closure(service)
connectionToService.invalidate()
return result
}
/// Ignores a specified release. `update` will be nil if the user has ignored the latest available release.
/// - Parameter release: The release to ignore.
public func ignore(release: Release) async {
@@ -101,10 +110,3 @@ extension Updater {
}
extension Updater {
enum Constants {
static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
}
}

View File

@@ -23,7 +23,7 @@ public protocol FileHandleWriter: Sendable {
extension FileHandle: FileHandleReader, FileHandleWriter {
public var pidOfConnectedProcess: Int32 {
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: 4, alignment: 1)
let pidPointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout<Int32>.size, alignment: 1)
var len = socklen_t(MemoryLayout<Int32>.size)
getsockopt(fileDescriptor, SOCK_STREAM, LOCAL_PEERPID, pidPointer, &len)
return pidPointer.load(as: Int32.self)