This reverts commit 629b63dfb8.
This commit is contained in:
parent
c1543ea49a
commit
2f08e1bee0
5 changed files with 1 additions and 522 deletions
|
|
@ -99,9 +99,7 @@
|
||||||
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; };
|
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; };
|
||||||
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */; };
|
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */; };
|
||||||
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008380 /* BrowserFindJavaScriptTests.swift */; };
|
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008380 /* BrowserFindJavaScriptTests.swift */; };
|
||||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
|
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
|
||||||
AB169902A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB169903A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift */; };
|
|
||||||
AB169900A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB169901A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift */; };
|
|
||||||
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
|
DA7A10CA710E000000000003 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000001 /* Localizable.xcstrings */; };
|
||||||
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
|
DA7A10CA710E000000000004 /* InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = DA7A10CA710E000000000002 /* InfoPlist.xcstrings */; };
|
||||||
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
|
A5001623 /* cmux.sdef in Resources */ = {isa = PBXBuildFile; fileRef = A5001622 /* cmux.sdef */; };
|
||||||
|
|
@ -267,8 +265,6 @@
|
||||||
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStressProfileTests.swift; sourceTree = "<group>"; };
|
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStressProfileTests.swift; sourceTree = "<group>"; };
|
||||||
A5008380 /* BrowserFindJavaScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFindJavaScriptTests.swift; sourceTree = "<group>"; };
|
A5008380 /* BrowserFindJavaScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFindJavaScriptTests.swift; sourceTree = "<group>"; };
|
||||||
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
|
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
|
||||||
AB169903A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateQuarantineRepairTests.swift; sourceTree = "<group>"; };
|
|
||||||
AB169901A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update/UpdateQuarantineRepair.swift; sourceTree = "<group>"; };
|
|
||||||
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
DA7A10CA710E000000000001 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
|
||||||
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
DA7A10CA710E000000000002 /* InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = InfoPlist.xcstrings; sourceTree = "<group>"; };
|
||||||
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
|
A5001622 /* cmux.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = cmux.sdef; sourceTree = "<group>"; };
|
||||||
|
|
@ -444,7 +440,6 @@
|
||||||
A5001221 /* UpdateTestSupport.swift */,
|
A5001221 /* UpdateTestSupport.swift */,
|
||||||
A5001224 /* UpdateTestURLProtocol.swift */,
|
A5001224 /* UpdateTestURLProtocol.swift */,
|
||||||
A5001223 /* UpdateLogStore.swift */,
|
A5001223 /* UpdateLogStore.swift */,
|
||||||
AB169901A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift */,
|
|
||||||
A5001217 /* UpdatePopoverView.swift */,
|
A5001217 /* UpdatePopoverView.swift */,
|
||||||
A5001218 /* UpdateTitlebarAccessory.swift */,
|
A5001218 /* UpdateTitlebarAccessory.swift */,
|
||||||
A5001219 /* WindowToolbarController.swift */,
|
A5001219 /* WindowToolbarController.swift */,
|
||||||
|
|
@ -524,7 +519,6 @@
|
||||||
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */,
|
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */,
|
||||||
A5008380 /* BrowserFindJavaScriptTests.swift */,
|
A5008380 /* BrowserFindJavaScriptTests.swift */,
|
||||||
A5008382 /* CommandPaletteSearchEngineTests.swift */,
|
A5008382 /* CommandPaletteSearchEngineTests.swift */,
|
||||||
AB169903A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift */,
|
|
||||||
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */,
|
970226F3C99D0D937CD00539 /* BrowserConfigTests.swift */,
|
||||||
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */,
|
58C7B1B978620BE162CC057E /* BrowserPanelTests.swift */,
|
||||||
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */,
|
02FC74F2C27127CC565B3E8C /* TerminalAndGhosttyTests.swift */,
|
||||||
|
|
@ -737,7 +731,6 @@
|
||||||
A500120B /* UpdateTestSupport.swift in Sources */,
|
A500120B /* UpdateTestSupport.swift in Sources */,
|
||||||
A500120E /* UpdateTestURLProtocol.swift in Sources */,
|
A500120E /* UpdateTestURLProtocol.swift in Sources */,
|
||||||
A500120D /* UpdateLogStore.swift in Sources */,
|
A500120D /* UpdateLogStore.swift in Sources */,
|
||||||
AB169900A1B2C3D4E5F60718 /* UpdateQuarantineRepair.swift in Sources */,
|
|
||||||
A5001207 /* UpdatePopoverView.swift in Sources */,
|
A5001207 /* UpdatePopoverView.swift in Sources */,
|
||||||
A5001208 /* UpdateTitlebarAccessory.swift in Sources */,
|
A5001208 /* UpdateTitlebarAccessory.swift in Sources */,
|
||||||
A5001209 /* WindowToolbarController.swift in Sources */,
|
A5001209 /* WindowToolbarController.swift in Sources */,
|
||||||
|
|
@ -785,7 +778,6 @@
|
||||||
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */,
|
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */,
|
||||||
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */,
|
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */,
|
||||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */,
|
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */,
|
||||||
AB169902A1B2C3D4E5F60718 /* UpdateQuarantineRepairTests.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -79,20 +79,6 @@ extension UpdateDriver: SPUUpdaterDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updater(_ updater: SPUUpdater, willExtractUpdate item: SUAppcastItem) {
|
|
||||||
prepareQuarantineRepair(for: item.fileURL)
|
|
||||||
do {
|
|
||||||
let result = try UpdateQuarantineRepair.repairDownloadedArchiveIfNeeded(
|
|
||||||
hostName: UpdateQuarantineRepair.sparkleHostName(),
|
|
||||||
versionString: item.versionString,
|
|
||||||
dataURL: item.fileURL
|
|
||||||
)
|
|
||||||
logUpdateQuarantineRepair(stage: "download", result: result)
|
|
||||||
} catch {
|
|
||||||
UpdateLogStore.shared.append("quarantine repair download failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) {
|
||||||
viewModel.clearDetectedUpdate()
|
viewModel.clearDetectedUpdate()
|
||||||
let nsError = error as NSError
|
let nsError = error as NSError
|
||||||
|
|
@ -125,13 +111,6 @@ extension UpdateDriver: SPUUpdaterDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func logUpdateQuarantineRepair(stage: String, result: UpdateQuarantineRepairResult) {
|
|
||||||
let path = result.url?.path ?? "<not-found>"
|
|
||||||
let before = result.beforeRawValue ?? "<none>"
|
|
||||||
let after = result.afterRawValue ?? "<none>"
|
|
||||||
UpdateLogStore.shared.append("quarantine repair \(stage): \(result.outcome) path=\(path) before=\(before) after=\(after)")
|
|
||||||
}
|
|
||||||
|
|
||||||
private func describeNoUpdateFoundReason(_ reason: SPUNoUpdateFoundReason) -> String {
|
private func describeNoUpdateFoundReason(_ reason: SPUNoUpdateFoundReason) -> String {
|
||||||
switch reason {
|
switch reason {
|
||||||
case .unknown:
|
case .unknown:
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||||
private var pendingCheckTransition: DispatchWorkItem?
|
private var pendingCheckTransition: DispatchWorkItem?
|
||||||
private var checkTimeoutWorkItem: DispatchWorkItem?
|
private var checkTimeoutWorkItem: DispatchWorkItem?
|
||||||
private var lastFeedURLString: String?
|
private var lastFeedURLString: String?
|
||||||
private var updateFileURLForQuarantineRepair: URL?
|
|
||||||
private var finishedExtractedUpdateQuarantineRepair: Bool = false
|
|
||||||
|
|
||||||
init(viewModel: UpdateViewModel, hostBundle _: Bundle) {
|
init(viewModel: UpdateViewModel, hostBundle _: Bundle) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
@ -120,13 +118,11 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||||
func showDownloadDidStartExtractingUpdate() {
|
func showDownloadDidStartExtractingUpdate() {
|
||||||
UpdateLogStore.shared.append("show extraction started")
|
UpdateLogStore.shared.append("show extraction started")
|
||||||
setState(.extracting(.init(progress: 0)))
|
setState(.extracting(.init(progress: 0)))
|
||||||
maybeRepairExtractedUpdateQuarantine()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showExtractionReceivedProgress(_ progress: Double) {
|
func showExtractionReceivedProgress(_ progress: Double) {
|
||||||
UpdateLogStore.shared.append(String(format: "show extraction progress: %.2f", progress))
|
UpdateLogStore.shared.append(String(format: "show extraction progress: %.2f", progress))
|
||||||
setState(.extracting(.init(progress: progress)))
|
setState(.extracting(.init(progress: progress)))
|
||||||
maybeRepairExtractedUpdateQuarantine()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
func showReady(toInstallAndRelaunch reply: @escaping @Sendable (SPUUserUpdateChoice) -> Void) {
|
||||||
|
|
@ -258,11 +254,6 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||||
UpdateLogStore.shared.append("feed url resolved\(suffix): \(feedURLString)")
|
UpdateLogStore.shared.append("feed url resolved\(suffix): \(feedURLString)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareQuarantineRepair(for updateFileURL: URL?) {
|
|
||||||
updateFileURLForQuarantineRepair = updateFileURL
|
|
||||||
finishedExtractedUpdateQuarantineRepair = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatErrorForLog(_ error: Error) -> String {
|
func formatErrorForLog(_ error: Error) -> String {
|
||||||
let nsError = error as NSError
|
let nsError = error as NSError
|
||||||
var parts: [String] = ["\(nsError.domain)(\(nsError.code))"]
|
var parts: [String] = ["\(nsError.domain)(\(nsError.code))"]
|
||||||
|
|
@ -311,24 +302,6 @@ class UpdateDriver: NSObject, SPUUserDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func maybeRepairExtractedUpdateQuarantine() {
|
|
||||||
guard !finishedExtractedUpdateQuarantineRepair else { return }
|
|
||||||
|
|
||||||
do {
|
|
||||||
let result = try UpdateQuarantineRepair.repairExtractedApplicationIfNeeded(dataURL: updateFileURLForQuarantineRepair)
|
|
||||||
guard result.outcome != .notFound else { return }
|
|
||||||
|
|
||||||
finishedExtractedUpdateQuarantineRepair = true
|
|
||||||
let path = result.url?.path ?? "<not-found>"
|
|
||||||
let before = result.beforeRawValue ?? "<none>"
|
|
||||||
let after = result.afterRawValue ?? "<none>"
|
|
||||||
UpdateLogStore.shared.append("quarantine repair extracted-app: \(result.outcome) path=\(path) before=\(before) after=\(after)")
|
|
||||||
} catch {
|
|
||||||
finishedExtractedUpdateQuarantineRepair = true
|
|
||||||
UpdateLogStore.shared.append("quarantine repair extracted-app failed: \(error.localizedDescription)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func runOnMain(_ action: @escaping () -> Void) {
|
private func runOnMain(_ action: @escaping () -> Void) {
|
||||||
if Thread.isMainThread {
|
if Thread.isMainThread {
|
||||||
action()
|
action()
|
||||||
|
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
import CoreServices
|
|
||||||
import Darwin
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
enum UpdateQuarantineRepairOutcome: Equatable {
|
|
||||||
case skipped
|
|
||||||
case notFound
|
|
||||||
case notQuarantined
|
|
||||||
case alreadyValid
|
|
||||||
case repaired
|
|
||||||
}
|
|
||||||
|
|
||||||
struct UpdateQuarantineRepairResult {
|
|
||||||
let outcome: UpdateQuarantineRepairOutcome
|
|
||||||
let url: URL?
|
|
||||||
let beforeRawValue: String?
|
|
||||||
let afterRawValue: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
enum UpdateQuarantineRepair {
|
|
||||||
static let sparkleCacheDirectoryName = "org.sparkle-project.Sparkle"
|
|
||||||
static let persistentDownloadsDirectoryName = "PersistentDownloads"
|
|
||||||
static let installationDirectoryName = "Installation"
|
|
||||||
|
|
||||||
private static let quarantineAttributeName = "com.apple.quarantine"
|
|
||||||
|
|
||||||
static func sparkleHostName(for bundle: Bundle = .main, fileManager: FileManager = .default) -> String {
|
|
||||||
for key in ["SUBundleName", "CFBundleDisplayName", kCFBundleNameKey as String] {
|
|
||||||
if let value = bundle.object(forInfoDictionaryKey: key) as? String,
|
|
||||||
!value.isEmpty {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (fileManager.displayName(atPath: bundle.bundlePath) as NSString).deletingPathExtension
|
|
||||||
}
|
|
||||||
|
|
||||||
static func persistentDownloadsRootURL(bundleIdentifier: String, cachesDirectory: URL? = nil) -> URL {
|
|
||||||
let base = cachesDirectory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory
|
|
||||||
return base
|
|
||||||
.appendingPathComponent(bundleIdentifier, isDirectory: true)
|
|
||||||
.appendingPathComponent(sparkleCacheDirectoryName, isDirectory: true)
|
|
||||||
.appendingPathComponent(persistentDownloadsDirectoryName, isDirectory: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func installationRootURL(bundleIdentifier: String, cachesDirectory: URL? = nil) -> URL {
|
|
||||||
let base = cachesDirectory ?? FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first ?? FileManager.default.temporaryDirectory
|
|
||||||
return base
|
|
||||||
.appendingPathComponent(bundleIdentifier, isDirectory: true)
|
|
||||||
.appendingPathComponent(sparkleCacheDirectoryName, isDirectory: true)
|
|
||||||
.appendingPathComponent(installationDirectoryName, isDirectory: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func locateDownloadedArchive(
|
|
||||||
bundleIdentifier: String,
|
|
||||||
hostName: String,
|
|
||||||
versionString: String,
|
|
||||||
cachesDirectory: URL? = nil,
|
|
||||||
fileManager: FileManager = .default
|
|
||||||
) -> URL? {
|
|
||||||
let rootURL = persistentDownloadsRootURL(bundleIdentifier: bundleIdentifier, cachesDirectory: cachesDirectory)
|
|
||||||
let expectedDirectoryName = (hostName.isEmpty || versionString.isEmpty) ? nil : "\(hostName) \(versionString)"
|
|
||||||
|
|
||||||
if let exactMatch = newestItem(
|
|
||||||
in: rootURL,
|
|
||||||
fileManager: fileManager,
|
|
||||||
skipPackageDescendants: true,
|
|
||||||
matching: { url, values, _ in
|
|
||||||
guard values.isRegularFile == true else { return false }
|
|
||||||
guard let expectedDirectoryName else { return true }
|
|
||||||
return url.deletingLastPathComponent().lastPathComponent == expectedDirectoryName
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return exactMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
return newestItem(in: rootURL, fileManager: fileManager, skipPackageDescendants: true) { _, values, _ in
|
|
||||||
values.isRegularFile == true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func locateExtractedApplication(
|
|
||||||
bundleIdentifier: String,
|
|
||||||
bundleName: String,
|
|
||||||
cachesDirectory: URL? = nil,
|
|
||||||
fileManager: FileManager = .default
|
|
||||||
) -> URL? {
|
|
||||||
let rootURL = installationRootURL(bundleIdentifier: bundleIdentifier, cachesDirectory: cachesDirectory)
|
|
||||||
let expectedBundleName = bundleName.isEmpty ? nil : bundleName
|
|
||||||
|
|
||||||
if let exactMatch = newestItem(
|
|
||||||
in: rootURL,
|
|
||||||
fileManager: fileManager,
|
|
||||||
skipPackageDescendants: true,
|
|
||||||
matching: { url, values, _ in
|
|
||||||
guard values.isDirectory == true, url.pathExtension == "app" else { return false }
|
|
||||||
guard let expectedBundleName else { return true }
|
|
||||||
return url.lastPathComponent == expectedBundleName
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return exactMatch
|
|
||||||
}
|
|
||||||
|
|
||||||
return newestItem(in: rootURL, fileManager: fileManager, skipPackageDescendants: true) { url, values, _ in
|
|
||||||
values.isDirectory == true && url.pathExtension == "app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func repairDownloadedArchiveIfNeeded(
|
|
||||||
hostName: String,
|
|
||||||
versionString: String,
|
|
||||||
bundle: Bundle = .main,
|
|
||||||
fileManager: FileManager = .default,
|
|
||||||
cachesDirectory: URL? = nil,
|
|
||||||
dataURL: URL? = nil
|
|
||||||
) throws -> UpdateQuarantineRepairResult {
|
|
||||||
guard let bundleIdentifier = bundle.bundleIdentifier else {
|
|
||||||
return .init(outcome: .skipped, url: nil, beforeRawValue: nil, afterRawValue: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let archiveURL = locateDownloadedArchive(
|
|
||||||
bundleIdentifier: bundleIdentifier,
|
|
||||||
hostName: hostName,
|
|
||||||
versionString: versionString,
|
|
||||||
cachesDirectory: cachesDirectory,
|
|
||||||
fileManager: fileManager
|
|
||||||
) else {
|
|
||||||
return .init(outcome: .notFound, url: nil, beforeRawValue: nil, afterRawValue: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return try repairQuarantineIfNeeded(
|
|
||||||
at: archiveURL,
|
|
||||||
agentBundleIdentifier: bundleIdentifier,
|
|
||||||
agentName: sparkleHostName(for: bundle, fileManager: fileManager),
|
|
||||||
dataURL: dataURL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func repairExtractedApplicationIfNeeded(
|
|
||||||
bundle: Bundle = .main,
|
|
||||||
fileManager: FileManager = .default,
|
|
||||||
cachesDirectory: URL? = nil,
|
|
||||||
dataURL: URL? = nil
|
|
||||||
) throws -> UpdateQuarantineRepairResult {
|
|
||||||
guard let bundleIdentifier = bundle.bundleIdentifier else {
|
|
||||||
return .init(outcome: .skipped, url: nil, beforeRawValue: nil, afterRawValue: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let appURL = locateExtractedApplication(
|
|
||||||
bundleIdentifier: bundleIdentifier,
|
|
||||||
bundleName: bundle.bundleURL.lastPathComponent,
|
|
||||||
cachesDirectory: cachesDirectory,
|
|
||||||
fileManager: fileManager
|
|
||||||
) else {
|
|
||||||
return .init(outcome: .notFound, url: nil, beforeRawValue: nil, afterRawValue: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return try repairQuarantineIfNeeded(
|
|
||||||
at: appURL,
|
|
||||||
agentBundleIdentifier: bundleIdentifier,
|
|
||||||
agentName: sparkleHostName(for: bundle, fileManager: fileManager),
|
|
||||||
dataURL: dataURL
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func repairQuarantineIfNeeded(
|
|
||||||
at url: URL,
|
|
||||||
agentBundleIdentifier: String,
|
|
||||||
agentName: String,
|
|
||||||
dataURL: URL? = nil
|
|
||||||
) throws -> UpdateQuarantineRepairResult {
|
|
||||||
let beforeRawValue = rawQuarantineAttribute(at: url)
|
|
||||||
var resourceValues = try url.resourceValues(forKeys: [.quarantinePropertiesKey])
|
|
||||||
var quarantineProperties = resourceValues.quarantineProperties ?? [:]
|
|
||||||
|
|
||||||
let hasQuarantine = beforeRawValue != nil || !quarantineProperties.isEmpty
|
|
||||||
guard hasQuarantine else {
|
|
||||||
return .init(outcome: .notQuarantined, url: url, beforeRawValue: beforeRawValue, afterRawValue: beforeRawValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
var didChange = false
|
|
||||||
|
|
||||||
let existingBundleIdentifier = (quarantineProperties[kLSQuarantineAgentBundleIdentifierKey as String] as? String)?
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if existingBundleIdentifier != agentBundleIdentifier {
|
|
||||||
quarantineProperties[kLSQuarantineAgentBundleIdentifierKey as String] = agentBundleIdentifier
|
|
||||||
didChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
let existingAgentName = (quarantineProperties[kLSQuarantineAgentNameKey as String] as? String)?
|
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
if existingAgentName != agentName {
|
|
||||||
quarantineProperties[kLSQuarantineAgentNameKey as String] = agentName
|
|
||||||
didChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if quarantineProperties[kLSQuarantineTypeKey as String] == nil {
|
|
||||||
quarantineProperties[kLSQuarantineTypeKey as String] = inferredQuarantineType(for: dataURL)
|
|
||||||
didChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if let dataURL, quarantineProperties[kLSQuarantineDataURLKey as String] == nil {
|
|
||||||
quarantineProperties[kLSQuarantineDataURLKey as String] = dataURL
|
|
||||||
didChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !didChange, let beforeRawValue, rawQuarantineNeedsLaunchServicesRepair(beforeRawValue) {
|
|
||||||
didChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
guard didChange else {
|
|
||||||
return .init(outcome: .alreadyValid, url: url, beforeRawValue: beforeRawValue, afterRawValue: beforeRawValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceValues.quarantineProperties = quarantineProperties
|
|
||||||
var mutableURL = url
|
|
||||||
try mutableURL.setResourceValues(resourceValues)
|
|
||||||
|
|
||||||
let afterRawValue = rawQuarantineAttribute(at: url)
|
|
||||||
return .init(outcome: .repaired, url: url, beforeRawValue: beforeRawValue, afterRawValue: afterRawValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rawQuarantineAttribute(at url: URL) -> String? {
|
|
||||||
url.path.withCString { pathPointer in
|
|
||||||
quarantineAttributeName.withCString { attributePointer in
|
|
||||||
let size = getxattr(pathPointer, attributePointer, nil, 0, 0, XATTR_NOFOLLOW)
|
|
||||||
guard size >= 0 else { return nil }
|
|
||||||
|
|
||||||
var buffer = [UInt8](repeating: 0, count: Int(size))
|
|
||||||
let bytesRead = getxattr(pathPointer, attributePointer, &buffer, buffer.count, 0, XATTR_NOFOLLOW)
|
|
||||||
guard bytesRead >= 0 else { return nil }
|
|
||||||
|
|
||||||
return String(decoding: buffer.prefix(Int(bytesRead)), as: UTF8.self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func rawQuarantineNeedsLaunchServicesRepair(_ rawValue: String) -> Bool {
|
|
||||||
let components = rawValue.split(separator: ";", omittingEmptySubsequences: false)
|
|
||||||
guard components.count >= 4 else { return true }
|
|
||||||
return components[3].isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func inferredQuarantineType(for dataURL: URL?) -> String {
|
|
||||||
guard let scheme = dataURL?.scheme?.lowercased() else {
|
|
||||||
return kLSQuarantineTypeOtherDownload as String
|
|
||||||
}
|
|
||||||
switch scheme {
|
|
||||||
case "http", "https":
|
|
||||||
return kLSQuarantineTypeWebDownload as String
|
|
||||||
default:
|
|
||||||
return kLSQuarantineTypeOtherDownload as String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func newestItem(
|
|
||||||
in rootURL: URL,
|
|
||||||
fileManager: FileManager,
|
|
||||||
skipPackageDescendants: Bool,
|
|
||||||
matching predicate: (URL, URLResourceValues, FileManager.DirectoryEnumerator) -> Bool
|
|
||||||
) -> URL? {
|
|
||||||
guard fileManager.fileExists(atPath: rootURL.path) else { return nil }
|
|
||||||
|
|
||||||
let keys: [URLResourceKey] = [.contentModificationDateKey, .isRegularFileKey, .isDirectoryKey]
|
|
||||||
guard let enumerator = fileManager.enumerator(
|
|
||||||
at: rootURL,
|
|
||||||
includingPropertiesForKeys: keys,
|
|
||||||
options: [.skipsHiddenFiles],
|
|
||||||
errorHandler: nil
|
|
||||||
) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var newestURL: URL?
|
|
||||||
var newestDate = Date.distantPast
|
|
||||||
|
|
||||||
for case let candidateURL as URL in enumerator {
|
|
||||||
let resourceValues = (try? candidateURL.resourceValues(forKeys: Set(keys))) ?? URLResourceValues()
|
|
||||||
if skipPackageDescendants && (candidateURL.pathExtension == "app" || candidateURL.pathExtension == "pkg") {
|
|
||||||
enumerator.skipDescendants()
|
|
||||||
}
|
|
||||||
guard predicate(candidateURL, resourceValues, enumerator) else { continue }
|
|
||||||
|
|
||||||
let contentModificationDate = resourceValues.contentModificationDate ?? Date.distantPast
|
|
||||||
if newestURL == nil || contentModificationDate > newestDate {
|
|
||||||
newestURL = candidateURL
|
|
||||||
newestDate = contentModificationDate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newestURL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,173 +0,0 @@
|
||||||
import CoreServices
|
|
||||||
import Darwin
|
|
||||||
import Foundation
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
#if canImport(cmux_DEV)
|
|
||||||
@testable import cmux_DEV
|
|
||||||
#elseif canImport(cmux)
|
|
||||||
@testable import cmux
|
|
||||||
#endif
|
|
||||||
|
|
||||||
final class UpdateQuarantineRepairTests: XCTestCase {
|
|
||||||
func testRepairAddsLaunchServicesMetadataForMissingAgentBundleIdentifier() throws {
|
|
||||||
let fileURL = try makeTemporaryFile(named: "cmux-nightly.dmg")
|
|
||||||
try writeRawQuarantine("0383;69ba4249;;", to: fileURL)
|
|
||||||
|
|
||||||
let beforeRawValue = try XCTUnwrap(UpdateQuarantineRepair.rawQuarantineAttribute(at: fileURL))
|
|
||||||
XCTAssertEqual(beforeRawValue, "0383;69ba4249;;")
|
|
||||||
|
|
||||||
let result = try UpdateQuarantineRepair.repairQuarantineIfNeeded(
|
|
||||||
at: fileURL,
|
|
||||||
agentBundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
agentName: "cmux NIGHTLY",
|
|
||||||
dataURL: URL(string: "https://example.com/cmux-nightly-macos.dmg")
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(result.outcome, .repaired)
|
|
||||||
let afterRawValue = try XCTUnwrap(UpdateQuarantineRepair.rawQuarantineAttribute(at: fileURL))
|
|
||||||
XCTAssertNotEqual(afterRawValue, beforeRawValue)
|
|
||||||
XCTAssertFalse(UpdateQuarantineRepair.rawQuarantineNeedsLaunchServicesRepair(afterRawValue))
|
|
||||||
|
|
||||||
let properties = try fileURL.resourceValues(forKeys: [.quarantinePropertiesKey]).quarantineProperties
|
|
||||||
XCTAssertEqual(properties?[kLSQuarantineAgentBundleIdentifierKey as String] as? String, "com.cmuxterm.app.nightly")
|
|
||||||
XCTAssertEqual(properties?[kLSQuarantineAgentNameKey as String] as? String, "cmux NIGHTLY")
|
|
||||||
XCTAssertEqual(properties?[kLSQuarantineTypeKey as String] as? String, kLSQuarantineTypeWebDownload as String)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRepairIsNoOpWhenLaunchServicesQuarantineRecordIsAlreadyValid() throws {
|
|
||||||
let fileURL = try makeTemporaryFile(named: "cmux-nightly.dmg")
|
|
||||||
try writeRawQuarantine("0383;69ba4249;;", to: fileURL)
|
|
||||||
|
|
||||||
_ = try UpdateQuarantineRepair.repairQuarantineIfNeeded(
|
|
||||||
at: fileURL,
|
|
||||||
agentBundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
agentName: "cmux NIGHTLY",
|
|
||||||
dataURL: URL(string: "https://example.com/cmux-nightly-macos.dmg")
|
|
||||||
)
|
|
||||||
|
|
||||||
let repairedRawValue = try XCTUnwrap(UpdateQuarantineRepair.rawQuarantineAttribute(at: fileURL))
|
|
||||||
let secondResult = try UpdateQuarantineRepair.repairQuarantineIfNeeded(
|
|
||||||
at: fileURL,
|
|
||||||
agentBundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
agentName: "cmux NIGHTLY",
|
|
||||||
dataURL: URL(string: "https://example.com/cmux-nightly-macos.dmg")
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(secondResult.outcome, .alreadyValid)
|
|
||||||
XCTAssertEqual(secondResult.beforeRawValue, repairedRawValue)
|
|
||||||
XCTAssertEqual(secondResult.afterRawValue, repairedRawValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLocateDownloadedArchivePrefersNewestMatchingVersionDirectory() throws {
|
|
||||||
let cachesDirectory = try makeTemporaryDirectory(named: "SparkleCaches")
|
|
||||||
let rootURL = UpdateQuarantineRepair.persistentDownloadsRootURL(
|
|
||||||
bundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
cachesDirectory: cachesDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
let oldArchiveURL = rootURL
|
|
||||||
.appendingPathComponent("token-old", isDirectory: true)
|
|
||||||
.appendingPathComponent("cmux NIGHTLY 1234", isDirectory: true)
|
|
||||||
.appendingPathComponent("old.dmg")
|
|
||||||
let newArchiveURL = rootURL
|
|
||||||
.appendingPathComponent("token-new", isDirectory: true)
|
|
||||||
.appendingPathComponent("cmux NIGHTLY 1234", isDirectory: true)
|
|
||||||
.appendingPathComponent("new.dmg")
|
|
||||||
let otherArchiveURL = rootURL
|
|
||||||
.appendingPathComponent("token-other", isDirectory: true)
|
|
||||||
.appendingPathComponent("cmux NIGHTLY 9999", isDirectory: true)
|
|
||||||
.appendingPathComponent("other.dmg")
|
|
||||||
|
|
||||||
try createFile(at: oldArchiveURL)
|
|
||||||
try createFile(at: newArchiveURL)
|
|
||||||
try createFile(at: otherArchiveURL)
|
|
||||||
|
|
||||||
try setModificationDate(Date(timeIntervalSince1970: 100), for: oldArchiveURL)
|
|
||||||
try setModificationDate(Date(timeIntervalSince1970: 200), for: newArchiveURL)
|
|
||||||
try setModificationDate(Date(timeIntervalSince1970: 300), for: otherArchiveURL)
|
|
||||||
|
|
||||||
let locatedArchiveURL = UpdateQuarantineRepair.locateDownloadedArchive(
|
|
||||||
bundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
hostName: "cmux NIGHTLY",
|
|
||||||
versionString: "1234",
|
|
||||||
cachesDirectory: cachesDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(locatedArchiveURL, newArchiveURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLocateExtractedApplicationUsesNewestMatchingBundleName() throws {
|
|
||||||
let cachesDirectory = try makeTemporaryDirectory(named: "SparkleInstallation")
|
|
||||||
let rootURL = UpdateQuarantineRepair.installationRootURL(
|
|
||||||
bundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
cachesDirectory: cachesDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
let oldAppURL = rootURL
|
|
||||||
.appendingPathComponent("install-old", isDirectory: true)
|
|
||||||
.appendingPathComponent("extract-old", isDirectory: true)
|
|
||||||
.appendingPathComponent("cmux NIGHTLY.app", isDirectory: true)
|
|
||||||
let newAppURL = rootURL
|
|
||||||
.appendingPathComponent("install-new", isDirectory: true)
|
|
||||||
.appendingPathComponent("extract-new", isDirectory: true)
|
|
||||||
.appendingPathComponent("cmux NIGHTLY.app", isDirectory: true)
|
|
||||||
let otherAppURL = rootURL
|
|
||||||
.appendingPathComponent("install-other", isDirectory: true)
|
|
||||||
.appendingPathComponent("extract-other", isDirectory: true)
|
|
||||||
.appendingPathComponent("Different.app", isDirectory: true)
|
|
||||||
|
|
||||||
try FileManager.default.createDirectory(at: oldAppURL, withIntermediateDirectories: true)
|
|
||||||
try FileManager.default.createDirectory(at: newAppURL, withIntermediateDirectories: true)
|
|
||||||
try FileManager.default.createDirectory(at: otherAppURL, withIntermediateDirectories: true)
|
|
||||||
|
|
||||||
try setModificationDate(Date(timeIntervalSince1970: 100), for: oldAppURL)
|
|
||||||
try setModificationDate(Date(timeIntervalSince1970: 200), for: newAppURL)
|
|
||||||
try setModificationDate(Date(timeIntervalSince1970: 300), for: otherAppURL)
|
|
||||||
|
|
||||||
let locatedAppURL = UpdateQuarantineRepair.locateExtractedApplication(
|
|
||||||
bundleIdentifier: "com.cmuxterm.app.nightly",
|
|
||||||
bundleName: "cmux NIGHTLY.app",
|
|
||||||
cachesDirectory: cachesDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
XCTAssertEqual(locatedAppURL, newAppURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeTemporaryDirectory(named name: String) throws -> URL {
|
|
||||||
let directoryURL = FileManager.default.temporaryDirectory
|
|
||||||
.appendingPathComponent("UpdateQuarantineRepairTests", isDirectory: true)
|
|
||||||
.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
||||||
.appendingPathComponent(name, isDirectory: true)
|
|
||||||
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
|
||||||
return directoryURL
|
|
||||||
}
|
|
||||||
|
|
||||||
private func makeTemporaryFile(named name: String) throws -> URL {
|
|
||||||
let directoryURL = try makeTemporaryDirectory(named: "Files")
|
|
||||||
let fileURL = directoryURL.appendingPathComponent(name)
|
|
||||||
try createFile(at: fileURL)
|
|
||||||
return fileURL
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createFile(at url: URL) throws {
|
|
||||||
try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
|
|
||||||
XCTAssertTrue(FileManager.default.createFile(atPath: url.path, contents: Data()))
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setModificationDate(_ modificationDate: Date, for url: URL) throws {
|
|
||||||
try FileManager.default.setAttributes([.modificationDate: modificationDate], ofItemAtPath: url.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func writeRawQuarantine(_ value: String, to url: URL) throws {
|
|
||||||
let bytes = Array(value.utf8)
|
|
||||||
let status = url.path.withCString { pathPointer in
|
|
||||||
"com.apple.quarantine".withCString { attributePointer in
|
|
||||||
bytes.withUnsafeBytes { bufferPointer in
|
|
||||||
setxattr(pathPointer, attributePointer, bufferPointer.baseAddress, bytes.count, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
XCTAssertEqual(status, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue