mirror of
https://github.com/mas-cli/mas
synced 2025-02-16 12:38:30 +00:00
Merge pull request #623 from rgoldberg/533-errors
Improve error messages
This commit is contained in:
commit
6d443993ec
29 changed files with 235 additions and 240 deletions
|
@ -10,21 +10,53 @@ import CommerceKit
|
|||
import PromiseKit
|
||||
import StoreFoundation
|
||||
|
||||
/// Downloads a list of apps, one after the other, printing progress to the console.
|
||||
/// Sequentially downloads apps, printing progress to the console.
|
||||
///
|
||||
/// Verifies that each supplied app ID is valid before attempting to download.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appIDs: The IDs of the apps to be downloaded
|
||||
/// - purchase: Flag indicating whether the apps needs to be purchased.
|
||||
/// Only works for free apps. Defaults to false.
|
||||
/// - unverifiedAppIDs: The app IDs of the apps to be verified and downloaded.
|
||||
/// - searcher: The `AppStoreSearcher` used to verify app IDs.
|
||||
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
|
||||
/// - Returns: A `Promise` that completes when the downloads are complete. If any fail,
|
||||
/// the promise is rejected with the first error, after all remaining downloads are attempted.
|
||||
func downloadApps(
|
||||
withAppIDs unverifiedAppIDs: [AppID],
|
||||
verifiedBy searcher: AppStoreSearcher,
|
||||
purchasing: Bool = false
|
||||
) -> Promise<Void> {
|
||||
when(resolved: unverifiedAppIDs.map { searcher.lookup(appID: $0) })
|
||||
.then { results in
|
||||
downloadApps(
|
||||
withAppIDs:
|
||||
results.compactMap { result in
|
||||
switch result {
|
||||
case .fulfilled(let searchResult):
|
||||
return searchResult.trackId
|
||||
case .rejected(let error):
|
||||
printError(String(describing: error))
|
||||
return nil
|
||||
}
|
||||
},
|
||||
purchasing: purchasing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequentially downloads apps, printing progress to the console.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appIDs: The app IDs of the apps to be downloaded.
|
||||
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
|
||||
/// - Returns: A promise that completes when the downloads are complete. If any fail,
|
||||
/// the promise is rejected with the first error, after all remaining downloads are attempted.
|
||||
func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
|
||||
func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) -> Promise<Void> {
|
||||
var firstError: Error?
|
||||
return
|
||||
appIDs
|
||||
.reduce(Guarantee.value(())) { previous, appID in
|
||||
previous.then {
|
||||
downloadWithRetries(appID, purchase: purchase)
|
||||
downloadApp(withAppID: appID, purchasing: purchasing)
|
||||
.recover { error in
|
||||
if firstError == nil {
|
||||
firstError = error
|
||||
|
@ -39,10 +71,15 @@ func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
|
|||
}
|
||||
}
|
||||
|
||||
private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise<Void> {
|
||||
SSPurchase().perform(appID: appID, purchase: purchase)
|
||||
private func downloadApp(
|
||||
withAppID appID: AppID,
|
||||
purchasing: Bool = false,
|
||||
withAttemptCount attemptCount: UInt32 = 3
|
||||
) -> Promise<Void> {
|
||||
SSPurchase()
|
||||
.perform(appID: appID, purchasing: purchasing)
|
||||
.recover { error in
|
||||
guard attempts > 1 else {
|
||||
guard attemptCount > 1 else {
|
||||
throw error
|
||||
}
|
||||
|
||||
|
@ -54,9 +91,9 @@ private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempt
|
|||
throw error
|
||||
}
|
||||
|
||||
let attempts = attempts - 1
|
||||
let attemptCount = attemptCount - 1
|
||||
printWarning((downloadError ?? error).localizedDescription)
|
||||
printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").")
|
||||
return downloadWithRetries(appID, purchase: purchase, attempts: attempts)
|
||||
printWarning("Trying again up to \(attemptCount) more \(attemptCount == 1 ? "time" : "times").")
|
||||
return downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import PromiseKit
|
|||
import StoreFoundation
|
||||
|
||||
extension SSPurchase {
|
||||
func perform(appID: AppID, purchase: Bool) -> Promise<Void> {
|
||||
func perform(appID: AppID, purchasing: Bool) -> Promise<Void> {
|
||||
var parameters: [String: Any] = [
|
||||
"productType": "C",
|
||||
"price": 0,
|
||||
|
@ -20,9 +20,11 @@ extension SSPurchase {
|
|||
"appExtVrsId": 0,
|
||||
]
|
||||
|
||||
if purchase {
|
||||
if purchasing {
|
||||
parameters["macappinstalledconfirmed"] = 1
|
||||
parameters["pricingParameters"] = "STDQ"
|
||||
// Possibly unnecessary…
|
||||
isRedownload = false
|
||||
} else {
|
||||
parameters["pricingParameters"] = "STDRDL"
|
||||
}
|
||||
|
@ -35,11 +37,6 @@ extension SSPurchase {
|
|||
|
||||
itemIdentifier = appID
|
||||
|
||||
// Not sure if this is needed…
|
||||
if purchase {
|
||||
isRedownload = false
|
||||
}
|
||||
|
||||
downloadMetadata = SSDownloadMetadata()
|
||||
downloadMetadata.kind = "software"
|
||||
downloadMetadata.itemIdentifier = appID
|
||||
|
|
|
@ -26,9 +26,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
let result = try searcher.lookup(appID: appID).wait()
|
||||
|
||||
guard let url = URL(string: result.trackViewUrl) else {
|
||||
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
||||
|
|
|
@ -27,11 +27,7 @@ extension MAS {
|
|||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
do {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
print(AppInfoFormatter.format(app: result))
|
||||
print(AppInfoFormatter.format(app: try searcher.lookup(appID: appID).wait()))
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
|
|
|
@ -18,15 +18,15 @@ extension MAS {
|
|||
|
||||
@Flag(help: "Force reinstall")
|
||||
var force = false
|
||||
@Argument(help: "App ID(s)")
|
||||
@Argument(help: ArgumentHelp("App ID", valueName: "app-id"))
|
||||
var appIDs: [AppID]
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIDs = appIDs.filter { appID in
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
|
||||
|
@ -38,7 +38,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
do {
|
||||
try downloadAll(appIDs).wait()
|
||||
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,6 @@ extension MAS {
|
|||
do {
|
||||
let results = try searcher.search(for: searchTerm).wait()
|
||||
guard let result = results.first else {
|
||||
printError("No results found")
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
|
@ -66,7 +65,7 @@ extension MAS {
|
|||
printWarning("\(appName) is already installed")
|
||||
} else {
|
||||
do {
|
||||
try downloadAll([appID]).wait()
|
||||
try downloadApps(withAppIDs: [appID]).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
|
|
|
@ -63,9 +63,7 @@ private func openMacAppStore() -> Promise<Void> {
|
|||
}
|
||||
|
||||
private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) throws {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.runtimeError("Unknown app ID \(appID)")
|
||||
}
|
||||
let result = try searcher.lookup(appID: appID).wait()
|
||||
|
||||
guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
|
||||
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
||||
|
|
|
@ -30,11 +30,22 @@ extension MAS {
|
|||
_ = try when(
|
||||
fulfilled:
|
||||
appLibrary.installedApps.map { installedApp in
|
||||
firstly {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.done { storeApp in
|
||||
guard let storeApp else {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
.done { storeApp in
|
||||
if installedApp.isOutdatedWhenComparedTo(storeApp) {
|
||||
print(
|
||||
"""
|
||||
\(installedApp.itemIdentifier) \(installedApp.appName) \
|
||||
(\(installedApp.bundleVersion) -> \(storeApp.version))
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
.recover { error in
|
||||
guard case MASError.unknownAppID = error else {
|
||||
throw error
|
||||
}
|
||||
|
||||
if verbose {
|
||||
printWarning(
|
||||
"""
|
||||
|
@ -43,18 +54,7 @@ extension MAS {
|
|||
"""
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if installedApp.isOutdatedWhenComparedTo(storeApp) {
|
||||
print(
|
||||
"""
|
||||
\(installedApp.itemIdentifier) \(installedApp.appName) \
|
||||
(\(installedApp.bundleVersion) -> \(storeApp.version))
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.wait()
|
||||
|
|
|
@ -15,15 +15,15 @@ extension MAS {
|
|||
abstract: "\"Purchase\" and install free apps from the Mac App Store"
|
||||
)
|
||||
|
||||
@Argument(help: "App ID(s)")
|
||||
@Argument(help: ArgumentHelp("App ID", valueName: "app-id"))
|
||||
var appIDs: [AppID]
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIDs = appIDs.filter { appID in
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
|
||||
|
@ -35,7 +35,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
do {
|
||||
try downloadAll(appIDs, purchase: true).wait()
|
||||
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher, purchasing: true).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
|
|
|
@ -32,12 +32,12 @@ extension MAS {
|
|||
throw MASError.macOSUserMustBeRoot
|
||||
}
|
||||
|
||||
guard let username = getSudoUsername() else {
|
||||
guard let username = ProcessInfo.processInfo.sudoUsername else {
|
||||
throw MASError.runtimeError("Could not determine the original username")
|
||||
}
|
||||
|
||||
guard
|
||||
let uid = getSudoUID(),
|
||||
let uid = ProcessInfo.processInfo.sudoUID,
|
||||
seteuid(uid) == 0
|
||||
else {
|
||||
throw MASError.runtimeError("Failed to switch effective user from 'root' to '\(username)'")
|
||||
|
|
|
@ -18,7 +18,7 @@ extension MAS {
|
|||
"Upgrade outdated app(s) installed from the Mac App Store"
|
||||
)
|
||||
|
||||
@Argument(help: "App ID(s)/app name(s)")
|
||||
@Argument(help: ArgumentHelp("App ID/app name", valueName: "app-id-or-name"))
|
||||
var appIDOrNames: [String] = []
|
||||
|
||||
/// Runs the command.
|
||||
|
@ -35,7 +35,6 @@ extension MAS {
|
|||
}
|
||||
|
||||
guard !apps.isEmpty else {
|
||||
printWarning("Nothing found to upgrade")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -46,7 +45,7 @@ extension MAS {
|
|||
)
|
||||
|
||||
do {
|
||||
try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
|
||||
try downloadApps(withAppIDs: apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
|
@ -59,28 +58,39 @@ extension MAS {
|
|||
let apps =
|
||||
appIDOrNames.isEmpty
|
||||
? appLibrary.installedApps
|
||||
: appIDOrNames.flatMap { appID in
|
||||
if let appID = AppID(appID) {
|
||||
: appIDOrNames.flatMap { appIDOrName in
|
||||
if let appID = AppID(appIDOrName) {
|
||||
// argument is an AppID, lookup apps by id using argument
|
||||
return appLibrary.installedApps(withAppID: appID)
|
||||
let installedApps = appLibrary.installedApps(withAppID: appID)
|
||||
if installedApps.isEmpty {
|
||||
printError(appID.unknownMessage)
|
||||
}
|
||||
return installedApps
|
||||
}
|
||||
|
||||
// argument is not an AppID, lookup apps by name using argument
|
||||
return appLibrary.installedApps(named: appID)
|
||||
let installedApps = appLibrary.installedApps(named: appIDOrName)
|
||||
if installedApps.isEmpty {
|
||||
printError("Unknown app name '\(appIDOrName)'")
|
||||
}
|
||||
return installedApps
|
||||
}
|
||||
|
||||
let promises = apps.map { installedApp in
|
||||
// only upgrade apps whose local version differs from the store version
|
||||
firstly {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.map { result -> (SoftwareProduct, SearchResult)? in
|
||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
return nil
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
.map { storeApp -> (SoftwareProduct, SearchResult)? in
|
||||
guard installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
return nil
|
||||
}
|
||||
return (installedApp, storeApp)
|
||||
}
|
||||
.recover { error -> Promise<(SoftwareProduct, SearchResult)?> in
|
||||
guard case MASError.unknownAppID = error else {
|
||||
return Promise(error: error)
|
||||
}
|
||||
return .value(nil)
|
||||
}
|
||||
|
||||
return (installedApp, storeApp)
|
||||
}
|
||||
}
|
||||
|
||||
return try when(fulfilled: promises).wait().compactMap { $0 }
|
||||
|
|
|
@ -26,9 +26,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
let result = try searcher.lookup(appID: appID).wait()
|
||||
|
||||
guard let urlString = result.sellerUrl else {
|
||||
throw MASError.noVendorWebsite
|
||||
|
|
|
@ -14,9 +14,11 @@ protocol AppStoreSearcher {
|
|||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appID: App ID.
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
||||
/// or an `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?>
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
|
||||
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
|
||||
/// An `Promise` for some other `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult>
|
||||
|
||||
/// Searches for apps.
|
||||
///
|
||||
/// - Parameter searchTerm: Term for which to search.
|
||||
|
|
|
@ -32,49 +32,46 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
|||
self.networkManager = networkManager
|
||||
}
|
||||
|
||||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appID: App ID.
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
||||
/// or an `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
|
||||
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
|
||||
/// An `Promise` for some other `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult> {
|
||||
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
|
||||
fatalError("Failed to build URL for \(appID)")
|
||||
}
|
||||
return firstly {
|
||||
return
|
||||
loadSearchResults(url)
|
||||
}
|
||||
.then { results -> Guarantee<SearchResult?> in
|
||||
guard let result = results.first else {
|
||||
return .value(nil)
|
||||
}
|
||||
|
||||
guard let pageURL = URL(string: result.trackViewUrl) else {
|
||||
return .value(result)
|
||||
}
|
||||
|
||||
return firstly {
|
||||
self.scrapeAppStoreVersion(pageURL)
|
||||
}
|
||||
.map { pageVersion in
|
||||
guard
|
||||
let pageVersion,
|
||||
let searchVersion = Version(tolerant: result.version),
|
||||
pageVersion > searchVersion
|
||||
else {
|
||||
return result
|
||||
.then { results -> Guarantee<SearchResult> in
|
||||
guard let result = results.first else {
|
||||
throw MASError.unknownAppID(appID)
|
||||
}
|
||||
|
||||
// Update the search result with the version from the App Store page.
|
||||
var result = result
|
||||
result.version = pageVersion.description
|
||||
return result
|
||||
guard let pageURL = URL(string: result.trackViewUrl) else {
|
||||
return .value(result)
|
||||
}
|
||||
|
||||
return
|
||||
self.scrapeAppStoreVersion(pageURL)
|
||||
.map { pageVersion in
|
||||
guard
|
||||
let pageVersion,
|
||||
let searchVersion = Version(tolerant: result.version),
|
||||
pageVersion > searchVersion
|
||||
else {
|
||||
return result
|
||||
}
|
||||
|
||||
// Update the search result with the version from the App Store page.
|
||||
var result = result
|
||||
result.version = pageVersion.description
|
||||
return result
|
||||
}
|
||||
.recover { _ in
|
||||
// If we were unable to scrape the App Store page, assume compatibility.
|
||||
.value(result)
|
||||
}
|
||||
}
|
||||
.recover { _ in
|
||||
// If we were unable to scrape the App Store page, assume compatibility.
|
||||
.value(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Searches for apps from the MAS.
|
||||
|
@ -106,36 +103,32 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
|||
}
|
||||
|
||||
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
||||
firstly {
|
||||
networkManager.loadData(from: url)
|
||||
}
|
||||
.map { data in
|
||||
do {
|
||||
return try JSONDecoder().decode(SearchResultList.self, from: data).results
|
||||
} catch {
|
||||
throw MASError.jsonParsing(data: data)
|
||||
networkManager.loadData(from: url)
|
||||
.map { data in
|
||||
do {
|
||||
return try JSONDecoder().decode(SearchResultList.self, from: data).results
|
||||
} catch {
|
||||
throw MASError.jsonParsing(data: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrape the app version from the App Store webpage at the given URL.
|
||||
///
|
||||
/// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API.
|
||||
private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise<Version?> {
|
||||
firstly {
|
||||
networkManager.loadData(from: pageURL)
|
||||
}
|
||||
.map { data in
|
||||
guard
|
||||
let html = String(data: data, encoding: .utf8),
|
||||
let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
|
||||
let version = Version(tolerant: capture)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
networkManager.loadData(from: pageURL)
|
||||
.map { data in
|
||||
guard
|
||||
let html = String(data: data, encoding: .utf8),
|
||||
let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
|
||||
let version = Version(tolerant: capture)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return version
|
||||
}
|
||||
return version
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the search URL for an app.
|
||||
|
|
|
@ -43,24 +43,6 @@ class SoftwareMapAppLibrary: AppLibrary {
|
|||
}
|
||||
}
|
||||
|
||||
func getSudoUsername() -> String? {
|
||||
ProcessInfo.processInfo.environment["SUDO_USER"]
|
||||
}
|
||||
|
||||
func getSudoUID() -> uid_t? {
|
||||
guard let uid = ProcessInfo.processInfo.environment["SUDO_UID"] else {
|
||||
return nil
|
||||
}
|
||||
return uid_t(uid)
|
||||
}
|
||||
|
||||
func getSudoGID() -> gid_t? {
|
||||
guard let gid = ProcessInfo.processInfo.environment["SUDO_GID"] else {
|
||||
return nil
|
||||
}
|
||||
return gid_t(gid)
|
||||
}
|
||||
|
||||
private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t) {
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: path)
|
||||
|
@ -75,11 +57,11 @@ private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t
|
|||
}
|
||||
|
||||
private func chown(paths: [String]) throws -> [String: (uid_t, gid_t)] {
|
||||
guard let sudoUID = getSudoUID() else {
|
||||
guard let sudoUID = ProcessInfo.processInfo.sudoUID else {
|
||||
throw MASError.runtimeError("Failed to get original uid")
|
||||
}
|
||||
|
||||
guard let sudoGID = getSudoGID() else {
|
||||
guard let sudoGID = ProcessInfo.processInfo.sudoGID else {
|
||||
throw MASError.runtimeError("Failed to get original gid")
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,9 @@ enum MASError: Error, Equatable {
|
|||
|
||||
case searchFailed
|
||||
case noSearchResultsFound
|
||||
|
||||
case unknownAppID(AppID)
|
||||
|
||||
case noVendorWebsite
|
||||
|
||||
case notInstalled(appID: AppID)
|
||||
|
@ -82,7 +85,9 @@ extension MASError: CustomStringConvertible {
|
|||
case .searchFailed:
|
||||
return "Search failed"
|
||||
case .noSearchResultsFound:
|
||||
return "No results found"
|
||||
return "No apps found"
|
||||
case .unknownAppID(let appID):
|
||||
return appID.unknownMessage
|
||||
case .noVendorWebsite:
|
||||
return "App does not have a vendor website"
|
||||
case .notInstalled(let appID):
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
//
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
@main
|
||||
|
@ -55,11 +54,3 @@ struct MAS: ParsableCommand {
|
|||
Self.initialize()
|
||||
}
|
||||
}
|
||||
|
||||
typealias AppID = UInt64
|
||||
|
||||
extension NSNumber {
|
||||
var appIDValue: AppID {
|
||||
uint64Value
|
||||
}
|
||||
}
|
||||
|
|
23
Sources/mas/Models/AppID.swift
Normal file
23
Sources/mas/Models/AppID.swift
Normal file
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// AppID.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ross Goldberg on 2024-10-29.
|
||||
// Copyright © 2024 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
typealias AppID = UInt64
|
||||
|
||||
extension AppID {
|
||||
var unknownMessage: String {
|
||||
"Unknown app ID \(self)"
|
||||
}
|
||||
}
|
||||
|
||||
extension NSNumber {
|
||||
var appIDValue: AppID {
|
||||
uint64Value
|
||||
}
|
||||
}
|
29
Sources/mas/Utilities/ProcessInfo.swift
Normal file
29
Sources/mas/Utilities/ProcessInfo.swift
Normal file
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// ProcessInfo.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ross Goldberg on 2024-10-29.
|
||||
// Copyright © 2024 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ProcessInfo {
|
||||
var sudoUsername: String? {
|
||||
environment["SUDO_USER"]
|
||||
}
|
||||
|
||||
var sudoUID: uid_t? {
|
||||
guard let uid = environment["SUDO_UID"] else {
|
||||
return nil
|
||||
}
|
||||
return uid_t(uid)
|
||||
}
|
||||
|
||||
var sudoGID: gid_t? {
|
||||
guard let gid = environment["SUDO_GID"] else {
|
||||
return nil
|
||||
}
|
||||
return gid_t(gid)
|
||||
}
|
||||
}
|
|
@ -22,28 +22,11 @@ public class HomeSpec: QuickSpec {
|
|||
beforeEach {
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try MAS.Home.parse(["--", "-999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try MAS.Home.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
it("opens app on MAS Preview") {
|
||||
let mockResult = SearchResult(
|
||||
trackId: 1111,
|
||||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try MAS.Home.parse([String(mockResult.trackId)]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,17 +23,11 @@ public class InfoSpec: QuickSpec {
|
|||
beforeEach {
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try MAS.Info.parse(["--", "-999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try MAS.Info.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
it("displays app details") {
|
||||
let mockResult = SearchResult(
|
||||
|
|
|
@ -19,7 +19,7 @@ public class InstallSpec: QuickSpec {
|
|||
xdescribe("install command") {
|
||||
xit("installs apps") {
|
||||
expect {
|
||||
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary())
|
||||
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -23,33 +23,11 @@ public class OpenSpec: QuickSpec {
|
|||
beforeEach {
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try MAS.Open.parse(["--", "-999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try MAS.Open.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
xit("opens app in MAS") {
|
||||
let mockResult = SearchResult(
|
||||
trackId: 1111,
|
||||
trackViewUrl: "fakescheme://some/url",
|
||||
version: "0.0"
|
||||
)
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try MAS.Open.parse([mockResult.trackId.description]).run(searcher: searcher)
|
||||
}
|
||||
}
|
||||
xit("just opens MAS if no app specified") {
|
||||
expect {
|
||||
try MAS.Open.parse([]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ public class PurchaseSpec: QuickSpec {
|
|||
xdescribe("purchase command") {
|
||||
xit("purchases apps") {
|
||||
expect {
|
||||
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary())
|
||||
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ public class UpgradeSpec: QuickSpec {
|
|||
.run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||
}
|
||||
}
|
||||
== "Warning: Nothing found to upgrade\n"
|
||||
.toNot(throwError())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,29 +22,11 @@ public class VendorSpec: QuickSpec {
|
|||
beforeEach {
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try MAS.Vendor.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
it("opens vendor app page in browser") {
|
||||
let mockResult = SearchResult(
|
||||
sellerUrl: "https://awesome.app",
|
||||
trackId: 1111,
|
||||
trackViewUrl: "https://apps.apple.com/us/app/awesome/id1111?mt=12&uo=4",
|
||||
version: "0.0"
|
||||
)
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try MAS.Vendor.parse([String(mockResult.trackId)]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ class MockAppStoreSearcher: AppStoreSearcher {
|
|||
.value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 })
|
||||
}
|
||||
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
||||
func lookup(appID: AppID) -> Promise<SearchResult> {
|
||||
guard let result = apps[appID] else {
|
||||
return Promise(error: MASError.noSearchResultsFound)
|
||||
return Promise(error: MASError.unknownAppID(appID))
|
||||
}
|
||||
|
||||
return .value(result)
|
||||
|
|
|
@ -100,7 +100,7 @@ class MASErrorTestCase: XCTestCase {
|
|||
|
||||
func testNoSearchResultsFound() {
|
||||
error = .noSearchResultsFound
|
||||
XCTAssertEqual(error.description, "No results found")
|
||||
XCTAssertEqual(error.description, "No apps found")
|
||||
}
|
||||
|
||||
func testNoVendorWebsite() {
|
||||
|
|
Loading…
Add table
Reference in a new issue