mirror of
https://github.com/TuringSoftware/CrystalFetch.git
synced 2025-12-24 14:29:48 +08:00
worker: implement convert process
This commit is contained in:
parent
1c5e8ad31f
commit
29d70d15f5
6 changed files with 144 additions and 11 deletions
|
|
@ -27,6 +27,7 @@ struct BuildConfigView: View {
|
|||
@State private var isConfirmCancelShown: Bool = false
|
||||
@State private var details = BuildDetails.empty
|
||||
@State private var edition = BuildEditions.empty
|
||||
@State private var isDownloadCompleted: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
|
|
@ -81,10 +82,19 @@ struct BuildConfigView: View {
|
|||
}
|
||||
Spacer()
|
||||
HStack {
|
||||
ProgressView(value: worker.progress) {
|
||||
|
||||
} currentValueLabel: {
|
||||
Text(worker.progressStatus ?? "")
|
||||
// SwiftUI BUG: ProgressView cannot go to indeterminate mode and back
|
||||
if let progress = worker.progress {
|
||||
ProgressView(value: progress) {
|
||||
|
||||
} currentValueLabel: {
|
||||
Text(worker.progressStatus ?? "")
|
||||
}
|
||||
} else {
|
||||
ProgressView(value: nil as Float?) {
|
||||
|
||||
} currentValueLabel: {
|
||||
Text(worker.progressStatus ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
HStack {
|
||||
|
|
@ -128,5 +138,18 @@ struct BuildConfigView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: worker.completedDownloadUrl) { newValue in
|
||||
if newValue != nil {
|
||||
isDownloadCompleted = true
|
||||
}
|
||||
}
|
||||
.fileMover(isPresented: $isDownloadCompleted, file: worker.completedDownloadUrl) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
worker.finalize(isoUrl: worker.completedDownloadUrl!, destinationUrl: success)
|
||||
case .failure(let failure):
|
||||
worker.lastSeenError = Worker.ErrorMessage(message: failure.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ struct ContentView: View {
|
|||
BuildsListView(arch: "arm64", hasPreviewBuilds: hasPreviewBuilds, hasServerBuilds: hasServerBuilds)
|
||||
#endif
|
||||
}.disabled(worker.isBusy)
|
||||
}.listStyle(.sidebar)
|
||||
}.frame(minWidth: 200, idealWidth: 300)
|
||||
.listStyle(.sidebar)
|
||||
.searchable(text: $worker.search, placement: .toolbar)
|
||||
.onSubmit(of: .search) {
|
||||
if !worker.isBusy {
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ actor Downloader {
|
|||
/// Start downloading a single item from the queue and retry if the download is interrupted
|
||||
private func dequeue() async throws {
|
||||
let (task, destinationUrl, retry) = queue.removeFirst()
|
||||
let debugIdentifier = task.originalRequest?.url?.absoluteString ?? "(unknown request)"
|
||||
NSLog("Downloading %@ to %@ (retries left: %d)", debugIdentifier, destinationUrl.path, retry)
|
||||
do {
|
||||
let resultUrl = try await withTaskCancellationHandler {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
|
|
@ -120,6 +122,7 @@ actor Downloader {
|
|||
try FileManager.default.moveItem(at: resultUrl, to: destinationUrl)
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
NSLog("Downloading %@ failed: ", debugIdentifier, error.localizedDescription)
|
||||
if retry > 0 {
|
||||
let newTask: URLSessionDownloadTask
|
||||
if let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ struct Main: App {
|
|||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView().environmentObject(worker)
|
||||
.frame(minWidth: 800, minHeight: 400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,12 +31,13 @@ class Worker: ObservableObject {
|
|||
@Published var builds: [UUPBuilds.Build] = []
|
||||
@Published var selectedBuild: UUPBuilds.Build?
|
||||
@Published var search: String = ""
|
||||
@Published private(set) var progress: Float = 0.0
|
||||
@Published private(set) var progress: Float?
|
||||
@Published private(set) var progressStatus: String?
|
||||
@Published var completedDownloadUrl: URL?
|
||||
|
||||
private let api = UUPDumpAPI()
|
||||
private var runningTask: (Task<Void, Never>)?
|
||||
private var lastErrorLine: String?
|
||||
|
||||
private var currentArch: String {
|
||||
#if arch(arm64)
|
||||
|
|
@ -81,13 +82,17 @@ class Worker: ObservableObject {
|
|||
builds.filter({ $0.arch == currentArch && (hasPreviewBuilds || !$0.title.contains("Insider")) && (hasServerBuilds || !$0.title.contains("Server")) }).first
|
||||
}
|
||||
|
||||
private func findIso(at url: URL) -> URL? {
|
||||
let files = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
||||
return files?.first(where: { $0.pathExtension.lowercased() == "iso" })
|
||||
}
|
||||
|
||||
func download(uuid: String, language: String, editions: [String]) {
|
||||
let cacheUrl = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||
let baseUrl = cacheUrl.appendingPathComponent(uuid)
|
||||
withBusyIndication { [self] in
|
||||
let fm = FileManager.default
|
||||
let files = try? fm.contentsOfDirectory(at: baseUrl, includingPropertiesForKeys: nil)
|
||||
if let existingUrl = files?.first(where: { $0.pathExtension == "iso" }) {
|
||||
if let existingUrl = findIso(at: baseUrl) {
|
||||
// if we already have the ISO, go ahead and return it
|
||||
completedDownloadUrl = existingUrl
|
||||
return
|
||||
|
|
@ -108,11 +113,95 @@ class Worker: ObservableObject {
|
|||
let total = ByteCountFormatter.string(fromByteCount: bytesTotal, countStyle: .file)
|
||||
Task { @MainActor in
|
||||
self.progressStatus = String.localizedStringWithFormat(NSLocalizedString("Downloading %@ of %@...", comment: "Worker"), written, total)
|
||||
self.progress = (Float(bytesWritten) / Float(bytesTotal)) * 0.9
|
||||
self.progress = (Float(bytesWritten) / Float(bytesTotal))
|
||||
}
|
||||
}
|
||||
progressStatus = NSLocalizedString("Converting download to ISO...", comment: "Worker")
|
||||
throw CancellationError()
|
||||
progress = nil
|
||||
try await convert(files: baseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private func convert(files url: URL) async throws {
|
||||
let script = Bundle.main.url(forResource: "convert", withExtension: "sh")!
|
||||
let executablePath = Bundle.main.executableURL!.deletingLastPathComponent().path
|
||||
let process = Process()
|
||||
process.executableURL = script
|
||||
process.currentDirectoryURL = url
|
||||
process.environment = ["PATH": "\(executablePath):/usr/bin:/bin:/usr/sbin:/sbin"]
|
||||
process.arguments = ["wim", ".", "1"]
|
||||
let outputPipe = Pipe()
|
||||
outputPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
if let line = String(data: handle.availableData, encoding: .ascii)?.filter({ $0.isASCII }), !line.isEmpty {
|
||||
NSLog("[convert.sh stdout]: %@", line)
|
||||
Task { @MainActor in
|
||||
self.progressStatus = line
|
||||
}
|
||||
}
|
||||
}
|
||||
let errorPipe = Pipe()
|
||||
errorPipe.fileHandleForReading.readabilityHandler = { handle in
|
||||
if let line = String(data: handle.availableData, encoding: .ascii)?.filter({ $0.isASCII }), !line.isEmpty {
|
||||
NSLog("[convert.sh stderr]: %@", line)
|
||||
Task { @MainActor in
|
||||
self.lastErrorLine = line
|
||||
}
|
||||
}
|
||||
}
|
||||
process.standardOutput = outputPipe
|
||||
process.standardError = errorPipe
|
||||
try await withTaskCancellationHandler {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
process.terminationHandler = { process in
|
||||
let status = process.terminationStatus
|
||||
if process.terminationReason == .exit && status == 0 {
|
||||
continuation.resume()
|
||||
} else if process.terminationReason == .uncaughtSignal && status == SIGTERM {
|
||||
continuation.resume(throwing: CancellationError())
|
||||
} else {
|
||||
Task { @MainActor in
|
||||
if let lastErrorLine = self.lastErrorLine {
|
||||
continuation.resume(throwing: WorkerError.conversionFailedMessage(lastErrorLine))
|
||||
} else {
|
||||
continuation.resume(throwing: WorkerError.conversionFailedUnknown(status))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
do {
|
||||
lastErrorLine = nil
|
||||
try process.run()
|
||||
} catch {
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
} onCancel: {
|
||||
process.terminate()
|
||||
}
|
||||
completedDownloadUrl = findIso(at: url)
|
||||
}
|
||||
|
||||
func finalize(isoUrl: URL, destinationUrl: URL) {
|
||||
let scoped = destinationUrl.startAccessingSecurityScopedResource()
|
||||
completedDownloadUrl = nil
|
||||
withBusyIndication {
|
||||
defer {
|
||||
if scoped {
|
||||
destinationUrl.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
self.progressStatus = NSLocalizedString("Saving ISO...", comment: "Worker")
|
||||
do {
|
||||
try FileManager.default.moveItem(at: isoUrl, to: destinationUrl)
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
if error.domain == NSCocoaErrorDomain && error.code == NSFileWriteFileExistsError {
|
||||
// ignore this error
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
try FileManager.default.removeItem(at: isoUrl.deletingLastPathComponent())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -122,6 +211,8 @@ class Worker: ObservableObject {
|
|||
|
||||
private func withBusyIndication(_ action: @escaping @MainActor () async throws -> Void) {
|
||||
isBusy = true
|
||||
progress = nil
|
||||
progressStatus = nil
|
||||
runningTask = Task {
|
||||
do {
|
||||
try await action()
|
||||
|
|
@ -137,3 +228,17 @@ class Worker: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum WorkerError: Error {
|
||||
case conversionFailedUnknown(Int32)
|
||||
case conversionFailedMessage(String)
|
||||
}
|
||||
|
||||
extension WorkerError: LocalizedError {
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .conversionFailedUnknown(let status): return String.localizedStringWithFormat(NSLocalizedString("The conversion script failed with error code %d", comment: "Worker"), status)
|
||||
case .conversionFailedMessage(let message): return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue