Skip to content

Commit dc5a8b0

Browse files
committed
WIP download runtime, refactor
1 parent 7325502 commit dc5a8b0

8 files changed

Lines changed: 297 additions & 61 deletions

File tree

Xcodes/Backend/AppState+Install.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,8 @@ extension AppState {
466466
let xcode = self.allXcodes[index]
467467
Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal)
468468
}
469-
}w func setInstallationStep(of runtime: DownloadableRuntime, to step: InstallationStep) {
469+
}
470+
func setInstallationStep(of runtime: DownloadableRuntime, to step: InstallationStep) {
470471
DispatchQueue.main.async {
471472

472473
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }

Xcodes/Backend/AppState+Runtimes.swift

Lines changed: 98 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -46,28 +46,38 @@ extension AppState {
4646
}
4747

4848
func downloadRuntime(runtime: DownloadableRuntime) {
49-
self.runtimePublishers[runtime.identifier] = downloadRunTimeFull(runtime: runtime)
50-
.receive(on: DispatchQueue.main)
51-
.sink(
52-
receiveCompletion: { [unowned self] completion in
53-
self.runtimePublishers[runtime.identifier] = nil
54-
if case let .failure(error) = completion {
55-
// // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
56-
// if error as? AuthenticationError != .invalidSession {
57-
// self.error = error
58-
// self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
59-
// }
60-
// if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
61-
// self.allXcodes[index].installState = .notInstalled
62-
// }
63-
}
64-
},
65-
receiveValue: { _ in }
66-
)
49+
Task {
50+
try? await downloadRunTimeFull(runtime: runtime)
51+
}
52+
53+
// self.runtimePublishers[runtime.identifier] = downloadRunTimeFull(runtime: runtime)
54+
// .receive(on: DispatchQueue.main)
55+
// .sink(
56+
// receiveCompletion: { [unowned self] completion in
57+
// self.runtimePublishers[runtime.identifier] = nil
58+
// if case let .failure(error) = completion {
59+
// Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
60+
//// // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead
61+
//// if error as? AuthenticationError != .invalidSession {
62+
//// self.error = error
63+
//// self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
64+
//// }
65+
//// if let index = self.allXcodes.firstIndex(where: { $0.id == id }) {
66+
//// self.allXcodes[index].installState = .notInstalled
67+
//// }
68+
// }
69+
// },
70+
// receiveValue: { _ in }
71+
// )
6772
}
6873

69-
func downloadRunTimeFull(runtime: DownloadableRuntime) -> AnyPublisher<(DownloadableRuntime, URL), Error> {
70-
// gets a proper cookie for runtimes
74+
func downloadRunTimeFull(runtime: DownloadableRuntime) async throws {
75+
// sets a proper cookie for runtimes
76+
try await validateADCSession(path: runtime.downloadPath)
77+
78+
let downloader = Downloader(rawValue: UserDefaults.standard.string(forKey: "downloader") ?? "aria2") ?? .aria2
79+
Logger.appState.info("Downloading \(runtime.visibleIdentifier) with \(downloader)")
80+
7181

7282
return validateADCSession(path: runtime.downloadPath)
7383
.flatMap { _ in
@@ -82,6 +92,18 @@ extension AppState {
8292
})
8393
.map { return (runtime, $0) }
8494
}
95+
.flatMap { runtime, url -> AnyPublisher<URL, Error> in
96+
switch runtime.contentType {
97+
case .package:
98+
return self.installFromPackage(dmgURL: url, runtime: runtime)
99+
case .diskImage:
100+
return self.installFromImage(dmgURL: url)
101+
}
102+
}
103+
.map { url in
104+
// Done deleting
105+
Logger.appState.debug("URL: \(url)")
106+
}
85107
.eraseToAnyPublisher()
86108
}
87109

@@ -92,7 +114,9 @@ extension AppState {
92114
// use runtime.url for final with cookies
93115

94116
// Check to see if the archive is in the expected path in case it was downloaded but failed to install
95-
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(runtime.name).\(runtime.name.suffix(fromLast: "."))"
117+
// let expectedRuntimePath = Path.xcodesApplicationSupport/"\(runtime.name).\(runtime.name.suffix(fromLast: "."))"
118+
let url = URL(string: runtime.source)!
119+
let expectedRuntimePath = Path.xcodesApplicationSupport/"\(url.lastPathComponent)"
96120
// aria2 downloads directly to the destination (instead of into /tmp first) so we need to make sure that the download isn't incomplete
97121
let aria2DownloadMetadataPath = expectedRuntimePath.parent/(expectedRuntimePath.basename() + ".aria2")
98122
var aria2DownloadIsIncomplete = false
@@ -106,7 +130,8 @@ extension AppState {
106130
.eraseToAnyPublisher()
107131
}
108132
else {
109-
// let destination = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).\(availableXcode.filename.suffix(fromLast: "."))"
133+
134+
Logger.appState.info("Downloading runtime: \(url.lastPathComponent)")
110135
switch downloader {
111136
case .aria2:
112137
let aria2Path = Path(url: Bundle.main.url(forAuxiliaryExecutable: "aria2c")!)!
@@ -115,12 +140,7 @@ extension AppState {
115140
to: expectedRuntimePath,
116141
aria2Path: aria2Path,
117142
progressChanged: progressChanged)
118-
// return downloadXcodeWithAria2(
119-
// availableXcode,
120-
// to: destination,
121-
// aria2Path: aria2Path,
122-
// progressChanged: progressChanged
123-
// )
143+
124144
case .urlSession:
125145

126146
return Just(runtime.url)
@@ -134,7 +154,6 @@ extension AppState {
134154
// )
135155
}
136156
}
137-
138157
}
139158

140159
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) -> AnyPublisher<URL, Error> {
@@ -151,4 +170,54 @@ extension AppState {
151170
.map { _ in destination.url }
152171
.eraseToAnyPublisher()
153172
}
173+
174+
public func downloadRuntimeWithAria2(_ runtime: DownloadableRuntime, to destination: Path, aria2Path: Path, progressChanged: @escaping (Progress) -> Void) async -> URL {
175+
176+
}
177+
178+
public func installFromImage(dmgURL: URL) -> AnyPublisher<URL, Error> {
179+
180+
181+
try? self.runtimeService.installRuntimeImage(dmgURL: dmgURL)
182+
183+
184+
return Just(dmgURL)
185+
.setFailureType(to: Error.self)
186+
.eraseToAnyPublisher()
187+
188+
}
189+
190+
public func installFromPackage(dmgURL: URL, runtime: DownloadableRuntime) -> AnyPublisher<URL, Error> {
191+
Logger.appState.info("Mounting DMG")
192+
Task {
193+
do {
194+
let mountedUrl = try await self.runtimeService.mountDMG(dmgUrl: dmgURL)
195+
196+
// 2-Get the first path under the mounted path, should be a .pkg
197+
let pkgPath = Path(url: mountedUrl)!.ls().first!
198+
try Path.xcodesCaches.mkdir().setCurrentUserAsOwner()
199+
200+
let expandedPkgPath = Path.xcodesCaches/runtime.identifier
201+
//try expandedPkgPath.mkdir()
202+
Logger.appState.info("PKG Path: \(pkgPath)")
203+
Logger.appState.info("Expanded PKG Path: \(expandedPkgPath)")
204+
//try? Current.files.removeItem(at: expandedPkgPath.url)
205+
206+
// 5-Expand (not install) the pkg to temporary path
207+
try await self.runtimeService.expand(pkgPath: pkgPath, expandedPkgPath: expandedPkgPath)
208+
//try await self.runtimeService.unmountDMG(mountedURL: mountedUrl)
209+
210+
} catch {
211+
Logger.appState.error("Error installing runtime: \(error.localizedDescription)")
212+
}
213+
214+
}
215+
216+
217+
218+
return Just(dmgURL)
219+
.setFailureType(to: Error.self)
220+
.eraseToAnyPublisher()
221+
}
222+
154223
}

Xcodes/Backend/AppState.swift

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,14 @@ class AppState: ObservableObject {
186186
.eraseToAnyPublisher()
187187
}
188188

189+
func validateADCSession(path: String) async throws {
190+
let result = try await Current.network.dataTaskAsync(with: URLRequest.downloadADCAuth(path: path))
191+
let httpResponse = result.1 as! HTTPURLResponse
192+
if httpResponse.statusCode == 401 {
193+
throw AuthenticationError.notAuthorized
194+
}
195+
}
196+
189197
func validateSession() -> AnyPublisher<Void, Error> {
190198

191199
return Current.network.validateSession()
@@ -799,30 +807,17 @@ class AppState: ObservableObject {
799807
}
800808

801809
// MARK: Runtimes
802-
func getRunTimes(xcode: Xcode) -> [DownloadableRuntime] {
803-
804-
let builds = xcode.sdks?.allBuilds()
805-
806-
let runtimes: [DownloadableRuntime]? = builds?.flatMap { sdkBuild in
807-
downloadableRuntimes.filter {
808-
$0.sdkBuildUpdate == sdkBuild
809-
}
810-
}
811-
812-
let updatedRuntimes = runtimes?.map { runtime in
813-
var updatedRuntime = runtime
814-
if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.sdkBuildUpdate }).first {
815-
let url = URL(fileURLWithPath: coreSimulatorInfo.path["relative"]!)
816-
updatedRuntime.installState = .installed(Path(url: url)!)
817-
} else {
818-
updatedRuntime.installState = .notInstalled
819-
}
820-
return updatedRuntime
810+
func runtimeInstallPath(xcode: Xcode, runtime: DownloadableRuntime) -> Path? {
811+
if let coreSimulatorInfo = installedRuntimes.filter({ $0.runtimeInfo.build == runtime.sdkBuildUpdate }).first {
812+
let urlString = coreSimulatorInfo.path["relative"]!
813+
// app was not allowed to open up file:// url's so remove
814+
let fileRemovedString = urlString.replacingOccurrences(of: "file://", with: "")
815+
let url = URL(fileURLWithPath: fileRemovedString)
816+
817+
return Path(url: url)!
821818
}
822-
823-
return updatedRuntimes ?? []
819+
return nil
824820
}
825-
826821

827822
// MARK: - Private
828823

Xcodes/Backend/Environment.swift

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,101 @@ public struct Shell {
112112
return (progress, publisher)
113113
}
114114

115+
public var downloadWithAria2Async: (Path, URL, Path, [HTTPCookie]) async throws -> Progress = { aria2Path, url, destination, cookies in
116+
let process = Process()
117+
process.executableURL = aria2Path.url
118+
process.arguments = [
119+
"--header=Cookie: \(cookies.map { "\($0.name)=\($0.value)" }.joined(separator: "; "))",
120+
"--max-connection-per-server=16",
121+
"--split=16",
122+
"--summary-interval=1",
123+
"--stop-with-process=\(ProcessInfo.processInfo.processIdentifier)", // if xcodes quits, stop aria2 process
124+
"--dir=\(destination.parent.string)",
125+
"--out=\(destination.basename())",
126+
"--human-readable=false", // sets the output to use bytes instead of formatting
127+
url.absoluteString,
128+
]
129+
let stdOutPipe = Pipe()
130+
process.standardOutput = stdOutPipe
131+
let stdErrPipe = Pipe()
132+
process.standardError = stdErrPipe
133+
134+
var progress = Progress()
135+
progress.kind = .file
136+
progress.fileOperationKind = .downloading
137+
138+
let observer = NotificationCenter.default.addObserver(
139+
forName: .NSFileHandleDataAvailable,
140+
object: nil,
141+
queue: OperationQueue.main
142+
) { note in
143+
guard
144+
// This should always be the case for Notification.Name.NSFileHandleDataAvailable
145+
let handle = note.object as? FileHandle,
146+
handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading
147+
else { return }
148+
149+
defer { handle.waitForDataInBackgroundAndNotify() }
150+
151+
let string = String(decoding: handle.availableData, as: UTF8.self)
152+
153+
progress.updateFromAria2(string: string)
154+
}
155+
156+
stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
157+
stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
158+
159+
do {
160+
161+
defer {
162+
//DispatchQueue.global(qos: .default).async {
163+
process.waitUntilExit()
164+
165+
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
166+
167+
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
168+
if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) {
169+
throw aria2cError
170+
} else {
171+
throw ProcessExecutionError(process: process, standardOutput: "", standardError: "")
172+
}
173+
}
174+
return
175+
// }
176+
}
177+
try process.run()
178+
} catch {
179+
throw error
180+
}
181+
182+
183+
// let publisher = Deferred {
184+
// Future<Void, Error> { promise in
185+
// DispatchQueue.global(qos: .default).async {
186+
// process.waitUntilExit()
187+
//
188+
// NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
189+
//
190+
// guard process.terminationReason == .exit, process.terminationStatus == 0 else {
191+
// if let aria2cError = Aria2CError(exitStatus: process.terminationStatus) {
192+
// return promise(.failure(aria2cError))
193+
// } else {
194+
// return promise(.failure(ProcessExecutionError(process: process, standardOutput: "", standardError: "")))
195+
// }
196+
// }
197+
// promise(.success(()))
198+
// }
199+
// }
200+
// }
201+
// .handleEvents(receiveCancel: {
202+
// process.terminate()
203+
// NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
204+
// })
205+
// .eraseToAnyPublisher()
206+
//
207+
// return (progress, publisher)
208+
}
209+
115210
public var unxipExperiment: (URL) -> AnyPublisher<ProcessOutput, Error> = { url in
116211
let unxipPath = Path(url: Bundle.main.url(forAuxiliaryExecutable: "unxip")!)!
117212
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
@@ -189,10 +284,15 @@ public struct Network {
189284
.mapError { $0 as Error }
190285
.eraseToAnyPublisher()
191286
}
287+
192288
public func dataTask(with request: URLRequest) -> AnyPublisher<URLSession.DataTaskPublisher.Output, Error> {
193289
dataTask(request)
194290
}
195-
291+
292+
public func dataTaskAsync(with request: URLRequest) async throws -> (Data, URLResponse) {
293+
return try await AppleAPI.Current.network.session.data(for: request)
294+
}
295+
196296
public var downloadTask: (URL, URL, Data?) -> (Progress, AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) = { AppleAPI.Current.network.session.downloadTask(with: $0, to: $1, resumingWith: $2) }
197297

198298
public func downloadTask(with url: URL, to saveLocation: URL, resumingWith resumeData: Data?) -> (progress: Progress, publisher: AnyPublisher<(saveLocation: URL, response: URLResponse), Error>) {

Xcodes/Backend/Path+.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,15 @@ extension Path {
3232
static var runtimeCacheFile: Path {
3333
return xcodesApplicationSupport/"downloadable-runtimes.json"
3434
}
35+
36+
static var xcodesCaches: Path {
37+
return caches/"com.xcodesorg.xcodesapp"
38+
}
39+
40+
@discardableResult
41+
func setCurrentUserAsOwner() -> Path {
42+
let user = ProcessInfo.processInfo.environment["SUDO_USER"] ?? NSUserName()
43+
try? FileManager.default.setAttributes([.ownerAccountName: user], ofItemAtPath: string)
44+
return self
45+
}
3546
}

0 commit comments

Comments
 (0)