Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
f28c2a4
🧱 Import only Foundation instead of Cocoa
chris-araman Apr 14, 2021
9916a9f
🏃 Throw any errors from Process.run
chris-araman Apr 14, 2021
cab6314
♻️ Remove unused extensions
chris-araman Apr 14, 2021
2891fe9
♻️ Remove unused PrivateFrameworks headers
chris-araman Apr 8, 2021
e6dd54b
🔍 Upgrade only to newer versions
chris-araman Apr 16, 2021
60cc16a
♻️ Refactor loadDataSync to reduce code duplication
chris-araman Apr 17, 2021
877cb62
♻️ Refactor MasStoreSearch to reduce code duplication
chris-araman Apr 17, 2021
763de0f
🔍 Examine app page for version newer than in search result
chris-araman Apr 17, 2021
4c99643
🏃 Load app pages asynchronously when scraping
chris-araman Apr 17, 2021
f505664
⏳ Simplify waits
chris-araman Apr 21, 2021
ecba9e8
🧹 Lint
chris-araman Apr 21, 2021
03ec323
🏃 Initialize NSRegularExpression once
chris-araman Apr 21, 2021
78c1541
♻️ Extract scrapeVersionFromPage
chris-araman Apr 21, 2021
4e4479f
♻️ Refactor StoreSearch into asynchronous methods
chris-araman Apr 21, 2021
5f9fc87
🤫 Silence remaining lint warnings
chris-araman Apr 21, 2021
2d96ca8
🏃 Run outdated faster by issuing multiple searches at the same time
chris-araman Apr 21, 2021
f3bfc1a
♻️ Refactor Upgrade to improve readability
chris-araman Apr 21, 2021
9b24cc8
♻️ Extract findOutdatedApps
chris-araman Apr 21, 2021
7f37916
🏃 Run upgrade faster by issuing multiple searches at the same time
chris-araman Apr 21, 2021
86a9fcc
♻️ Simplify network closures
chris-araman Apr 22, 2021
6b8e91b
♻️ Pass around simple array of SearchResult
chris-araman Apr 22, 2021
a27cc68
🙈 hide unnecessary publics and @objc types
chris-araman Apr 22, 2021
6a12b30
💣 Fail fast if regular expression can not be initialized
chris-araman Apr 22, 2021
a85d4e8
🛡 Rephrase as guard statements
chris-araman Apr 22, 2021
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
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"AllPublicDeclarationsHaveDocumentation" : false,
"AlwaysUseLowerCamelCase" : true,
"AmbiguousTrailingClosureOverload" : true,
"BeginDocumentationCommentWithOneLineSummary" : true,
"BeginDocumentationCommentWithOneLineSummary" : false,
"DoNotUseSemicolons" : true,
"DontRepeatTypeInStaticProperties" : true,
"FileScopedDeclarationPrivacy" : true,
Expand Down
2 changes: 1 addition & 1 deletion MasKit/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func download(_ adamId: UInt64, purchase: Bool = false) -> MASError? {
}
}

_ = group.wait(timeout: .distantFuture)
group.wait()

if let observerIdentifier = observerIdentifier {
CKDownloadQueue.shared().remove(observerIdentifier)
Expand Down
9 changes: 8 additions & 1 deletion MasKit/Commands/Home.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ public struct HomeCommand: CommandProtocol {
private let storeSearch: StoreSearch
private var openCommand: ExternalCommand

public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
)
}

/// Designated initializer.
public init(
init(
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
Expand Down
6 changes: 5 additions & 1 deletion MasKit/Commands/Info.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ public struct InfoCommand: CommandProtocol {

private let storeSearch: StoreSearch

public init() {
self.init(storeSearch: MasStoreSearch())
}

/// Designated initializer.
public init(storeSearch: StoreSearch = MasStoreSearch()) {
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.storeSearch = storeSearch
}

Expand Down
2 changes: 1 addition & 1 deletion MasKit/Commands/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct InstallCommand: CommandProtocol {
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let downloadResults = options.appIds.compactMap { (appId) -> MASError? in
let downloadResults = options.appIds.compactMap { appId -> MASError? in
if let product = appLibrary.installedApp(forId: appId), !options.forceInstall {
printWarning("\(product.appName) is already installed")
return nil
Expand Down
12 changes: 8 additions & 4 deletions MasKit/Commands/Lucky.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ public struct LuckyCommand: CommandProtocol {
private let appLibrary: AppLibrary
private let storeSearch: StoreSearch

/// Public initializer.
public init() {
self.init(storeSearch: MasStoreSearch())
}

/// Designated initializer.
/// - Parameter storeSearch: Search manager.
public init(storeSearch: StoreSearch = MasStoreSearch()) {
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch)
}

Expand All @@ -42,7 +46,7 @@ public struct LuckyCommand: CommandProtocol {

do {
let results = try storeSearch.search(for: options.appName)
guard let result = results.results.first else {
guard let result = results.first else {
print("No results found")
return .failure(.noSearchResultsFound)
}
Expand Down Expand Up @@ -70,7 +74,7 @@ public struct LuckyCommand: CommandProtocol {
fileprivate func install(_ appId: UInt64, options: Options) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let downloadResults = [appId]
.compactMap { (appId) -> MASError? in
.compactMap { appId -> MASError? in
if let product = appLibrary.installedApp(forId: appId), !options.forceInstall {
printWarning("\(product.appName) is already installed")
return nil
Expand Down
9 changes: 8 additions & 1 deletion MasKit/Commands/Open.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ public struct OpenCommand: CommandProtocol {
private let storeSearch: StoreSearch
private var systemOpen: ExternalCommand

public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
)
}

/// Designated initializer.
public init(
init(
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
Expand Down
42 changes: 29 additions & 13 deletions MasKit/Commands/Outdated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,44 @@ public struct OutdatedCommand: CommandProtocol {

/// Runs the command.
public func run(_: Options) -> Result<Void, MASError> {
var failure: MASError?
let group = DispatchGroup()
for installedApp in appLibrary.installedApps {
do {
if let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue) {
if installedApp.bundleVersion != storeApp.version {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
""")
}
} else {
group.enter()
storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { storeApp, error in
defer { group.leave() }

guard error == nil else {
// Bubble up MASErrors
failure = error as? MASError ?? .searchFailed
return
}

guard let storeApp = storeApp else {
printWarning(
"""
Identifier \(installedApp.itemIdentifier) not found in store. \
Was expected to identify \(installedApp.appName).
""")
return
}

if installedApp.isOutdatedWhenComparedTo(storeApp) {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
""")
}
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
}
}

group.wait()

if let failure = failure {
return .failure(failure)
}

return .success(())
}
}
2 changes: 1 addition & 1 deletion MasKit/Commands/Purchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public struct PurchaseCommand: CommandProtocol {
/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
// Try to download applications with given identifiers and collect results
let downloadResults = options.appIds.compactMap { (appId) -> MASError? in
let downloadResults = options.appIds.compactMap { appId -> MASError? in
if let product = appLibrary.installedApp(forId: appId) {
printWarning("\(product.appName) has already been purchased.")
return nil
Expand Down
12 changes: 8 additions & 4 deletions MasKit/Commands/Search.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,26 @@ public struct SearchCommand: CommandProtocol {

private let storeSearch: StoreSearch

public init() {
self.init(storeSearch: MasStoreSearch())
}

/// Designated initializer.
///
/// - Parameter storeSearch: Search manager.
public init(storeSearch: StoreSearch = MasStoreSearch()) {
init(storeSearch: StoreSearch = MasStoreSearch()) {
self.storeSearch = storeSearch
}

public func run(_ options: Options) -> Result<Void, MASError> {
do {
let resultList = try storeSearch.search(for: options.appName)
if resultList.resultCount <= 0 || resultList.results.isEmpty {
let results = try storeSearch.search(for: options.appName)
if results.isEmpty {
print("No results found")
return .failure(.noSearchResultsFound)
}

let output = SearchResultFormatter.format(results: resultList.results, includePrice: options.price)
let output = SearchResultFormatter.format(results: results, includePrice: options.price)
print(output)

return .success(())
Expand Down
110 changes: 64 additions & 46 deletions MasKit/Commands/Upgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,64 +32,82 @@ public struct UpgradeCommand: CommandProtocol {

/// Runs the command.
public func run(_ options: Options) -> Result<Void, MASError> {
let apps: [SoftwareProduct]
do {
let apps =
try
(options.apps.count == 0
? appLibrary.installedApps
: options.apps.compactMap {
if let appId = UInt64($0) {
// if argument a UInt64, lookup app by id using argument
return appLibrary.installedApp(forId: appId)
} else {
// if argument not a UInt64, lookup app by name using argument
return appLibrary.installedApp(named: $0)
}
})
.compactMap { (installedApp: SoftwareProduct) -> SoftwareProduct? in
// only upgrade apps whose local version differs from the store version
if let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue) {
return storeApp.version != installedApp.bundleVersion
? installedApp
: nil
} else {
return nil
}
}
apps = try findOutdatedApps(options)
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
}

guard apps.count > 0 else {
printWarning("Nothing found to upgrade")
return .success(())
guard apps.count > 0 else {
printWarning("Nothing found to upgrade")
return .success(())
}

print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
print(apps.map { "\($0.appName) (\($0.bundleVersion))" }.joined(separator: ", "))

var updatedAppCount = 0
var failedUpgradeResults = [MASError]()
for app in apps {
if let upgradeResult = download(app.itemIdentifier.uint64Value) {
failedUpgradeResults.append(upgradeResult)
} else {
updatedAppCount += 1
}
}

print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):")
print(apps.map { "\($0.appName) (\($0.bundleVersion))" }.joined(separator: ", "))
switch failedUpgradeResults.count {
case 0:
if updatedAppCount == 0 {
print("Everything is up-to-date")
}
return .success(())
case 1:
return .failure(failedUpgradeResults[0])
default:
return .failure(.downloadFailed(error: nil))
}
}

var updatedAppCount = 0
var failedUpgradeResults = [MASError]()
for app in apps {
if let upgradeResult = download(app.itemIdentifier.uint64Value) {
failedUpgradeResults.append(upgradeResult)
private func findOutdatedApps(_ options: Options) throws -> [SoftwareProduct] {
var apps: [SoftwareProduct]
if options.apps.isEmpty {
apps = appLibrary.installedApps
} else {
apps = options.apps.compactMap {
if let appId = UInt64($0) {
// if argument a UInt64, lookup app by id using argument
return appLibrary.installedApp(forId: appId)
} else {
updatedAppCount += 1
// if argument not a UInt64, lookup app by name using argument
return appLibrary.installedApp(named: $0)
}
}
}

var outdated = [SoftwareProduct]()
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 1)
for installedApp in apps {
// only upgrade apps whose local version differs from the store version
group.enter()
storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { result, _ in
defer { group.leave() }

if let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) {
semaphore.wait()
defer { semaphore.signal() }

switch failedUpgradeResults.count {
case 0:
if updatedAppCount == 0 {
print("Everything is up-to-date")
outdated.append(installedApp)
}
return .success(())
case 1:
return .failure(failedUpgradeResults[0])
default:
return .failure(.downloadFailed(error: nil))
}
} catch {
// Bubble up MASErrors
return .failure(error as? MASError ?? .searchFailed)
}

group.wait()

return outdated
}
}

Expand Down
9 changes: 8 additions & 1 deletion MasKit/Commands/Vendor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@ public struct VendorCommand: CommandProtocol {
private let storeSearch: StoreSearch
private var openCommand: ExternalCommand

public init() {
self.init(
storeSearch: MasStoreSearch(),
openCommand: OpenSystemCommand()
)
}

/// Designated initializer.
public init(
init(
storeSearch: StoreSearch = MasStoreSearch(),
openCommand: ExternalCommand = OpenSystemCommand()
) {
Expand Down
Loading