8000 Improve concurrency & other cleanup by rgoldberg · Pull Request #771 · mas-cli/mas · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Improve concurrency & other cleanup #771

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@
"version" : "7.5.0"
}
},
{
"identity" : "regex",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sharplet/Regex.git",
"state" : {
"revision" : "76c2b73d4281d77fc3118391877efd1bf972f515",
"version" : "2.1.1"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
Expand Down
2 changes: 0 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ let package = Package(
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
.package(url: "https://github.com/funky-monkey/IsoCountryCodes.git", from: "1.0.2"),
.package(url: "https://github.com/mxcl/Version.git", from: "2.1.0"),
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"),
],
targets: [
.executableTarget(
name: "mas",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
"IsoCountryCodes",
"Regex",
"Version",
],
swiftSettings: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@
import StoreFoundation

// MARK: - SoftwareProduct
extension CKSoftwareProduct: SoftwareProduct {}
extension CKSoftwareProduct: SoftwareProduct, @retroactive @unchecked Sendable {}
35 changes: 27 additions & 8 deletions Sources/mas/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright (c) 2015 Andrew Naylor. All rights reserved.
//

import CommerceKit
import StoreFoundation

/// Sequentially downloads apps, printing p 8000 rogress to the console.
Expand Down Expand Up @@ -33,7 +34,7 @@ func downloadApps(
printWarning("App ID \(appID) not found in Mac App Store.")
continue
}
try await downloadApp(withAppID: appID, purchasing: purchasing)
try await downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: 3)
}
}

Expand All @@ -45,17 +46,13 @@ func downloadApps(
/// - Throws: If a download fails, immediately throws an error.
func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) async throws {
for appID in appIDs {
try await downloadApp(withAppID: appID, purchasing: purchasing)
try await downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: 3)
}
}

private func downloadApp(
withAppID appID: AppID,
purchasing: Bool = false,
withAttemptCount attemptCount: UInt32 = 3
) async throws {
private func downloadApp(withAppID appID: AppID, purchasing: Bool, withAttemptCount attemptCount: UInt32) async throws {
do {
try await SSPurchase().perform(appID: appID, purchasing: purchasing)
try await downloadApp(withAppID: appID, purchasing: purchasing)
} catch {
guard attemptCount > 1 else {
throw error
Expand All @@ -75,3 +72,25 @@ private func downloadApp(
try await downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
}
}

private func downloadApp(withAppID appID: AppID, purchasing: Bool = false) async throws {
_ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
CKPurchaseController.shared()
.perform(SSPurchase(appID: appID, purchasing: purchasing), withOptions: 0) { _, _, error, response in
if let error {
continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError))
} else if response?.downloads.isEmpty == false {
Task {
do {
try await PurchaseDownloadObserver(appID: appID).observeDownloadQueue()
continuation.resume()
} catch {
continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError))
}
}
} else {
continuation.resume(throwing: MASError.noDownloads)
}
}
}
}
10 changes: 5 additions & 5 deletions Sources/mas/AppStore/PurchaseDownloadObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ private let initialPhase = 4 as Int64
private let downloadedPhase = 5 as Int64

class PurchaseDownloadObserver: CKDownloadQueueObserver {
private let purchase: SSPurchase
private let appID: AppID
private var completionHandler: (() -> Void)?
private var errorHandler: ((MASError) -> Void)?
private var priorPhaseType: Int64?

init(purchase: SSPurchase) {
self.purchase = purchase
init(appID: AppID) {
self.appID = appID
}

deinit {
Expand All @@ -29,7 +29,7 @@ class PurchaseDownloadObserver: CKDownloadQueueObserver {

func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
guard
download.metadata.itemIdentifier == purchase.itemIdentifier,
download.metadata.itemIdentifier == appID,
let status = download.status
else {
return
Expand Down Expand Up @@ -68,7 +68,7 @@ class PurchaseDownloadObserver: CKDownloadQueueObserver {

func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
guard
download.metadata.itemIdentifier == purchase.itemIdentifier,
download.metadata.itemIdentifier == appID,
let status = download.status
else {
return
Expand Down
38 changes: 10 additions & 28 deletions Sources/mas/AppStore/SSPurchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
import CommerceKit

extension SSPurchase {
func perform(appID: AppID, purchasing: Bool) async throws {
convenience init(appID: AppID, purchasing: Bool) {
self.init()

var parameters =
[
"productType": "C",
Expand Down Expand Up @@ -44,33 +46,13 @@ extension SSPurchase {
// redownloads without passing any account IDs to SSPurchase.
// https://github.com/mas-cli/mas/issues/417
if #unavailable(macOS 12) {
let storeAccount = try ISStoreAccount.primaryAccount
accountIdentifier = storeAccount.dsID
appleID = storeAccount.identifier
}

try await perform()
}

private func perform() async throws {
let _: Void = try await withCheckedThrowingContinuation { continuation in
CKPurchaseController.shared()
.perform(self, withOptions: 0) { purchase, _, error, response in
if let error {
continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError))
} else if response?.downloads.isEmpty == false, let purchase {
Task {
do {
try await PurchaseDownloadObserver(purchase: purchase).observeDownloadQueue()
continuation.resume()
} catch {
continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError))
}
}
} else {
continuation.resume(throwing: MASError.noDownloads)
}
}
do {
let storeAccount = try ISStoreAccount.primaryAccount
accountIdentifier = storeAccount.dsID
appleID = storeAccount.identifier
} catch {
// do nothing
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ extension MAS {
}

private func configStringValue(_ name: String) -> String {
var size = MemoryLayout<Int32>.size
var size = 0
guard sysctlbyname(name, nil, &size, nil, 0) == 0 else {
perror("sysctlbyname")
return unknown
Expand All @@ -64,5 +64,5 @@ private func configStringValue(_ name: String) -> String {
return unknown
}

return String(cString: buffer)
return String(cString: buffer, encoding: .utf8) ?? unknown
}
4 changes: 2 additions & 2 deletions Sources/mas/Commands/Install.swift 10000
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ extension MAS {
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) async throws {
// Try to download applications with given identifiers and collect results
let appIDs = appIDs.filter { appID in
if let displayName = appLibrary.installedApps(withAppID: appID).first?.displayName, !force {
printWarning("\(displayName) is already installed")
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
printWarning("\(appName) is already installed")
return false
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/mas/Commands/Lucky.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ extension MAS {
/// - Throws: Any error that occurs while attempting to install the app.
private func install(appID: AppID, appLibrary: AppLibrary) async throws {
// Try to download applications with given identifiers and collect results
if let displayName = appLibrary.installedApps(withAppID: appID).first?.displayName, !force {
printWarning("\(displayName) is already installed")
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
printWarning("\(appName) is already installed")
} else {
do {
try await downloadApps(withAppIDs: [appID])
Expand Down
4 changes: 2 additions & 2 deletions Sources/mas/Commands/Outdated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ extension MAS {
if installedApp.isOutdated(comparedTo: storeApp) {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.displayName) \
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
"""
)
Expand All @@ -41,7 +41,7 @@ extension MAS {
printWarning(
"""
Identifier \(installedApp.itemIdentifier) not found in store. \
Was expected to identify \(installedApp.displayName).
Was expected to identify \(installedApp.appName).
"""
)
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/mas/Commands/Purchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ extension MAS {
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) async throws {
// Try to download applications with given identifiers and collect results
let appIDs = appIDs.filter { appID in
if let displayName = appLibrary.installedApps(withAppID: appID).first?.displayName {
printWarning("\(displayName) has already been purchased.")
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
printWarning("\(appName) has already been purchased.")
return false
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Uninstall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ extension MAS {

if dryRun {
for installedApp in installedApps {
printInfo("'\(installedApp.displayName)' '\(installedApp.bundlePath)'")
printInfo("'\(installedApp.appName)' '\(installedApp.bundlePath)'")
}
printInfo("(not removed, dry run)")
} else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/mas/Commands/Upgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ extension MAS {
printWarning(
"""
Identifier \(installedApp.itemIdentifier) not found in store. \
Was expected to identify \(installedApp.displayName).
Was expected to identify \(installedApp.appName).
"""
)
}
Expand Down
88 changes: 50 additions & 38 deletions Sources/mas/Controllers/SpotlightSoftwareMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,63 @@

import Foundation

struct SpotlightSoftwareMap: SoftwareMap {
class SpotlightSoftwareMap: SoftwareMap {
private var observer: NSObjectProtocol?

deinit {
// do nothing
}

@MainActor
func allSoftwareProducts() async -> [SoftwareProduct] {
await withCheckedContinuation { continuation in
DispatchQueue.main.async {
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "kMDItemAppStoreAdamID LIKE '*'")
query.searchScopes = ["/Applications"]
defer {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}

var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(
forName: Notification.Name.NSMetadataQueryDidFinishGathering,
object: query,
queue: .main
) { [weak observer] _ in
query.stop()
if let observer {
NotificationCenter.default.removeObserver(observer)
}
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "kMDItemAppStoreAdamID LIKE '*'")
query.searchScopes = ["/Applications"]

continuation.resume(
returning: query.results.compactMap { result in
guard let result = result as? NSMetadataItem else {
return nil
}
return await withCheckedContinuation { continuation in
observer = NotificationCenter.default.addObserver(
forName: .NSMetadataQueryDidFinishGathering,
object: query,
queue: nil
) { notification in
guard let query = notification.object as? NSMetadataQuery else {
continuation.resume(returning: [])
return
}

query.stop()

return SimpleSoftwareProduct(
appName:
(result.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "")
.removeSuffix(".app"),
bundleIdentifier:
result.value(forAttribute: kMDItemCFBundleIdentifier as String) as? String ?? "",
bundlePath:
result.value(forAttribute: kMDItemPath as String) as? String ?? "",
bundleVersion:
result.value(forAttribute: kMDItemVersion as String) as? String ?? "",
itemIdentifier:
// swiftlint:disable:next legacy_objc_type
result.value(forAttribute: "kMDItemAppStoreAdamID") as? NSNumber ?? 0
)
continuation.resume(
returning: query.results.compactMap { result in
guard let result = result as? NSMetadataItem else {
return nil
}
)
}

query.start()
return SimpleSoftwareProduct(
appName:
(result.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "")
.removeSuffix(".app"),
bundleIdentifier:
result.value(forAttribute: kMDItemCFBundleIdentifier as String) as? String ?? "",
bundlePath:
result.value(forAttribute: kMDItemPath as String) as? String ?? "",
bundleVersion:
result.value(forAttribute: kMDItemVersion as String) as? String ?? "",
itemIdentifier:
// swiftlint:disable:next legacy_objc_type
result.value(forAttribute: "kMDItemAppStoreAdamID") as? NSNumber ?? 0
)
}
)
}

query.start()
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions Sources/mas/Formatters/AppListFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,18 @@ enum AppListFormatter {
/// - Parameter products: List of software products app data.
/// - Returns: Multiline text output.
static func format(products: [SoftwareProduct]) -> String {
// find longest displayName for formatting
let maxLength = products.map(\.displayName.count).max() ?? 0
// find longest appName for formatting
let maxLength = products.map(\.appName.count).max() ?? 0

var output = ""

for product in products {
let appID = product.itemIdentifier.stringValue
.padding(toLength: idColumnMinWidth, withPad: " ", startingAt: 0)
let displayName = product.displayName.padding(toLength: maxLength, withPad: " ", startingAt: 0)
let appName = product.appName.padding(toLength: maxLength, withPad: " ", startingAt: 0)
let version = product.bundleVersion

output += "\(appID) \(displayName) (\(version))\n"
output += "\(appID) \(appName) (\(version))\n"
}

return output.trimmingCharacters(in: .newlines)
Expand Down
Loading
0