Skip to content

Commit

Permalink
Remove usage of Swift concurrency (#76)
Browse files Browse the repository at this point in the history
* Remove usage of Swift concurrency
* Update integration tests workflow
  • Loading branch information
majd authored May 29, 2022
1 parent 119c553 commit 8c591f0
Show file tree
Hide file tree
Showing 17 changed files with 181 additions and 126 deletions.
8 changes: 1 addition & 7 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,5 @@ jobs:
path: ipatool-current
- name: Update permissions
run: chmod +x ./ipatool-current/ipatool
- name: Install Swift toolchain
run: |
wget https://download.swift.org/swift-5.5.3-release/xcode/swift-5.5.3-RELEASE/swift-5.5.3-RELEASE-osx.pkg
sudo installer -pkg swift-5.5.3-RELEASE-osx.pkg -target /
- name: Run tests
run: |
export DYLD_LIBRARY_PATH=/Library/Developer/Toolchains/swift-5.5.3-RELEASE.xctoolchain/usr/lib/swift/macosx
./ipatool-current/ipatool ${{ matrix.command }} --help
run: ./ipatool-current/ipatool ${{ matrix.command }} --help
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.DS_Store
.AppleDouble
.LSOverride
.vscode/

# Icon must end with two \r
Icon
Expand Down
18 changes: 9 additions & 9 deletions Sources/CLI/Commands/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Networking
import StoreAPI
import Persistence

struct Auth: AsyncParsableCommand {
struct Auth: ParsableCommand {
static var configuration: CommandConfiguration {
return .init(
commandName: "auth",
Expand All @@ -23,7 +23,7 @@ struct Auth: AsyncParsableCommand {
}

extension Auth {
struct Login: AsyncParsableCommand {
struct Login: ParsableCommand {
static var configuration: CommandConfiguration {
return .init(abstract: "Login to the App Store.")
}
Expand All @@ -43,7 +43,7 @@ extension Auth {
lazy var logger = ConsoleLogger(level: logLevel)
}

struct Revoke: AsyncParsableCommand {
struct Revoke: ParsableCommand {
static var configuration: CommandConfiguration {
return .init(abstract: "Revoke your App Store credentials.")
}
Expand Down Expand Up @@ -95,7 +95,7 @@ extension Auth.Login {
}
}

private mutating func authenticate(email: String, password: String) async -> Account {
private mutating func authenticate(email: String, password: String) -> Account {
logger.log("Creating HTTP client...", level: .debug)
let httpClient = HTTPClient(session: URLSession.shared)

Expand All @@ -104,7 +104,7 @@ extension Auth.Login {

do {
logger.log("Authenticating with the App Store...", level: .info)
let account = try await storeClient.authenticate(email: email, password: password, code: nil)
let account = try storeClient.authenticate(email: email, password: password, code: nil)
return Account(
name: "\(account.firstName) \(account.lastName)",
email: email,
Expand All @@ -115,7 +115,7 @@ extension Auth.Login {
switch error {
case StoreResponse.Error.codeRequired:
do {
let account = try await storeClient.authenticate(email: email, password: password, code: authCode())
let account = try storeClient.authenticate(email: email, password: password, code: authCode())
return Account(
name: "\(account.firstName) \(account.lastName)",
email: email,
Expand Down Expand Up @@ -161,15 +161,15 @@ extension Auth.Login {
}
}

mutating func run() async throws {
mutating func run() throws {
// Get Apple ID email
let email: String = email()

// Get Apple ID password
let password: String = password()

// Authenticate with the App Store
let account: Account = await authenticate(email: email, password: password)
let account: Account = authenticate(email: email, password: password)

// Store data in keychain
do {
Expand All @@ -187,7 +187,7 @@ extension Auth.Login {
}

extension Auth.Revoke {
mutating func run() async throws {
mutating func run() throws {
let keychainStore = KeychainStore(service: "ipatool.service")

guard let account: Account = try keychainStore.value(forKey: "account") else {
Expand Down
30 changes: 15 additions & 15 deletions Sources/CLI/Commands/Download.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Networking
import StoreAPI
import Persistence

struct Download: AsyncParsableCommand {
struct Download: ParsableCommand {
static var configuration: CommandConfiguration {
return .init(abstract: "Download (encrypted) iOS app packages from the App Store.")
}
Expand Down Expand Up @@ -41,7 +41,7 @@ struct Download: AsyncParsableCommand {
}

extension Download {
private mutating func app(with bundleIdentifier: String, countryCode: String) async -> iTunesResponse.Result {
private mutating func app(with bundleIdentifier: String, countryCode: String) -> iTunesResponse.Result {
logger.log("Creating HTTP client...", level: .debug)
let httpClient = HTTPClient(session: URLSession.shared)

Expand All @@ -50,7 +50,7 @@ extension Download {

do {
logger.log("Querying the iTunes Store for '\(bundleIdentifier)' in country '\(countryCode)'...", level: .info)
return try await itunesClient.lookup(
return try itunesClient.lookup(
bundleIdentifier: bundleIdentifier,
countryCode: countryCode,
deviceFamily: deviceFamily
Expand All @@ -70,7 +70,7 @@ extension Download {
}


private mutating func purchase(app: iTunesResponse.Result, account: Account) async {
private mutating func purchase(app: iTunesResponse.Result, account: Account) {
guard app.price == 0 else {
logger.log("It is only possible to obtain a license for free apps. Purchase the app manually and run the \"download\" command again.", level: .error)
_exit(1)
Expand All @@ -84,7 +84,7 @@ extension Download {

do {
logger.log("Obtaining a license for '\(app.identifier)' from the App Store...", level: .info)
try await storeClient.purchase(
try storeClient.purchase(
identifier: "\(app.identifier)",
directoryServicesIdentifier: account.directoryServicesIdentifier,
passwordToken: account.passwordToken,
Expand Down Expand Up @@ -116,7 +116,7 @@ extension Download {
from app: iTunesResponse.Result,
account: Account,
purchaseAttempted: Bool = false
) async -> StoreResponse.Item {
) -> StoreResponse.Item {
logger.log("Creating HTTP client...", level: .debug)
let httpClient = HTTPClient(session: URLSession.shared)

Expand All @@ -125,7 +125,7 @@ extension Download {

do {
logger.log("Requesting a signed copy of '\(app.identifier)' from the App Store...", level: .info)
return try await storeClient.item(
return try storeClient.item(
identifier: "\(app.identifier)",
directoryServicesIdentifier: account.directoryServicesIdentifier
)
Expand All @@ -141,10 +141,10 @@ extension Download {
if !purchaseAttempted, purchase {
logger.log("License is missing.", level: .info)

await purchase(app: app, account: account)
purchase(app: app, account: account)
logger.log("Obtained a license for '\(app.identifier)'.", level: .debug)

return await item(from: app, account: account, purchaseAttempted: true)
return item(from: app, account: account, purchaseAttempted: true)
} else {
logger.log("Your Apple ID does not have a license for this app. Use the \"purchase\" command or the \"--purchase\" to obtain a license.", level: .error)
}
Expand All @@ -160,13 +160,13 @@ extension Download {
}
}

private mutating func download(item: StoreResponse.Item, to targetURL: URL) async {
private mutating func download(item: StoreResponse.Item, to targetURL: URL) {
logger.log("Creating download client...", level: .debug)
let downloadClient = HTTPDownloadClient()

do {
logger.log("Downloading app package...", level: .info)
try await downloadClient.download(from: item.url, to: targetURL) { [logger] progress in
try downloadClient.download(from: item.url, to: targetURL) { [logger] progress in
logger.log("Downloading app package... [\(Int((progress * 100).rounded()))%]",
prefix: "\u{1B}[1A\u{1B}[K",
level: .info)
Expand Down Expand Up @@ -216,7 +216,7 @@ extension Download {
}
}

mutating func run() async throws {
mutating func run() throws {
// Authenticate with the App Store
let keychainStore = KeychainStore(service: "ipatool.service")

Expand All @@ -227,19 +227,19 @@ extension Download {
logger.log("Authenticated as '\(account.name)'.", level: .info)

// Query for app
let app: iTunesResponse.Result = await app(with: bundleIdentifier, countryCode: countryCode)
let app: iTunesResponse.Result = app(with: bundleIdentifier, countryCode: countryCode)
logger.log("Found app: \(app.name) (\(app.version)).", level: .debug)

// Query for store item
let item: StoreResponse.Item = await item(from: app, account: account)
let item: StoreResponse.Item = item(from: app, account: account)
logger.log("Received a response of the signed copy: \(item.md5).", level: .debug)

// Generate file name
let path = makeOutputPath(app: app)
logger.log("Output path: \(path).", level: .debug)

// Download app package
await download(item: item, to: URL(fileURLWithPath: path))
download(item: item, to: URL(fileURLWithPath: path))
logger.log("Saved app package to \(URL(fileURLWithPath: path).lastPathComponent).", level: .info)

// Apply patches
Expand Down
2 changes: 1 addition & 1 deletion Sources/CLI/Commands/IPATool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import ArgumentParser

@main
struct IPATool: AsyncParsableCommand {
struct IPATool: ParsableCommand {
static var configuration: CommandConfiguration {
return CommandConfiguration(
commandName: "ipatool",
Expand Down
16 changes: 8 additions & 8 deletions Sources/CLI/Commands/Purchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Networking
import StoreAPI
import Persistence

struct Purchase: AsyncParsableCommand {
struct Purchase: ParsableCommand {
static var configuration: CommandConfiguration {
return .init(abstract: "Obtain a license for the app from the App Store.")
}
Expand All @@ -35,7 +35,7 @@ struct Purchase: AsyncParsableCommand {
}

extension Purchase {
private mutating func app(with bundleIdentifier: String) async -> iTunesResponse.Result {
private mutating func app(with bundleIdentifier: String) -> iTunesResponse.Result {
logger.log("Creating HTTP client...", level: .debug)
let httpClient = HTTPClient(session: URLSession.shared)

Expand All @@ -44,7 +44,7 @@ extension Purchase {

do {
logger.log("Querying the iTunes Store for '\(bundleIdentifier)' in country '\(countryCode)'...", level: .info)
let app = try await itunesClient.lookup(
let app = try itunesClient.lookup(
bundleIdentifier: bundleIdentifier,
countryCode: countryCode,
deviceFamily: deviceFamily
Expand All @@ -70,7 +70,7 @@ extension Purchase {
}
}

private mutating func purchase(app: iTunesResponse.Result, account: Account) async {
private mutating func purchase(app: iTunesResponse.Result, account: Account) {
logger.log("Creating HTTP client...", level: .debug)
let httpClient = HTTPClient(session: URLSession.shared)

Expand All @@ -79,7 +79,7 @@ extension Purchase {

do {
logger.log("Obtaining a license for '\(app.identifier)' from the App Store...", level: .info)
try await storeClient.purchase(
try storeClient.purchase(
identifier: "\(app.identifier)",
directoryServicesIdentifier: account.directoryServicesIdentifier,
passwordToken: account.passwordToken,
Expand Down Expand Up @@ -107,7 +107,7 @@ extension Purchase {
}
}

mutating func run() async throws {
mutating func run() throws {
// Authenticate with the App Store
let keychainStore = KeychainStore(service: "ipatool.service")

Expand All @@ -118,11 +118,11 @@ extension Purchase {
logger.log("Authenticated as '\(account.name)'.", level: .info)

// Query for app
let app: iTunesResponse.Result = await app(with: bundleIdentifier)
let app: iTunesResponse.Result = app(with: bundleIdentifier)
logger.log("Found app: \(app.name) (\(app.version)).", level: .debug)

// Obtain a license
await purchase(app: app, account: account)
purchase(app: app, account: account)
logger.log("Obtained a license for '\(app.identifier)'.", level: .debug)
logger.log("Done.", level: .info)
}
Expand Down
10 changes: 5 additions & 5 deletions Sources/CLI/Commands/Search.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
import Networking
import StoreAPI

struct Search: AsyncParsableCommand {
struct Search: ParsableCommand {
static var configuration: CommandConfiguration {
return .init(abstract: "Search for iOS apps available on the App Store.")
}
Expand All @@ -37,7 +37,7 @@ struct Search: AsyncParsableCommand {
}

extension Search {
mutating func results(with term: String) async -> [iTunesResponse.Result] {
mutating func results(with term: String) -> [iTunesResponse.Result] {
logger.log("Creating HTTP client...", level: .debug)
let httpClient = HTTPClient(session: URLSession.shared)

Expand All @@ -47,7 +47,7 @@ extension Search {
logger.log("Searching for '\(term)' using the '\(countryCode)' store front...", level: .info)

do {
let results = try await itunesClient.search(
let results = try itunesClient.search(
term: term,
limit: limit,
countryCode: countryCode,
Expand All @@ -67,9 +67,9 @@ extension Search {
}
}

mutating func run() async throws {
mutating func run() throws {
// Search the iTunes store
let results = await results(with: term)
let results = results(with: term)

// Compile output
let output = results
Expand Down
24 changes: 18 additions & 6 deletions Sources/Networking/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

public protocol HTTPClientInterface {
func send(_ request: HTTPRequest) async throws -> HTTPResponse
func send(_ request: HTTPRequest) throws -> HTTPResponse
}

public final class HTTPClient: HTTPClientInterface {
Expand All @@ -18,15 +18,27 @@ public final class HTTPClient: HTTPClientInterface {
self.session = session
}

public func send(_ request: HTTPRequest) async throws -> HTTPResponse {
public func send(_ request: HTTPRequest) throws -> HTTPResponse {
let request = try makeURLRequest(from: request)
let (data, response) = try await session.data(for: request)
let semaphore = DispatchSemaphore(value: 0)
var result: (data: Data?, response: URLResponse?, error: Swift.Error?)

guard let response = response as? HTTPURLResponse else {
throw Error.invalidResponse(response)
session.dataTask(with: request) { (data, response, error) in
result = (data, response, error)
semaphore.signal()
}.resume()

semaphore.wait()

if let error = result.error {
throw error
}

guard let response = result.response as? HTTPURLResponse else {
throw Error.invalidResponse(result.response)
}

return HTTPResponse(statusCode: response.statusCode, data: data)
return HTTPResponse(statusCode: response.statusCode, data: result.data)
}

private func makeURLRequest(from request: HTTPRequest) throws -> URLRequest {
Expand Down
Loading

0 comments on commit 8c591f0

Please sign in to comment.