* Log progress

* ProgressObserver

* Reverted run configuration

* Cache *.ipsw files

* Use fraction

* Use datatask and pre-create cache folder
This commit is contained in:
Fedor Korotkov 2022-03-22 22:17:23 -04:00 committed by GitHub
parent 483627904c
commit dc1e502404
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 14 deletions

View File

@ -0,0 +1,46 @@
import Foundation
public protocol Logger {
func appendNewLine(_ line: String) -> Void
func updateLastLine(_ line: String) -> Void
}
var defaultLogger: Logger = {
if ProcessInfo.processInfo.environment["CI"] != nil {
return SimpleConsoleLogger()
} else {
return InteractiveConsoleLogger()
}
}()
public class InteractiveConsoleLogger: Logger {
private let eraseCursorDown = "\u{001B}[J" // clear entire line
private let moveUp = "\u{001B}[1A" // move one line up
private let moveBeginningOfLine = "\r" //
public init() {
}
public func appendNewLine(_ line: String) {
print(line, terminator: "\n")
}
public func updateLastLine(_ line: String) {
print(moveUp, moveBeginningOfLine, eraseCursorDown, line, separator: "", terminator: "\n")
}
}
public class SimpleConsoleLogger: Logger {
public init() {
}
public func appendNewLine(_ line: String) {
print(line, terminator: "\n")
}
public func updateLastLine(_ line: String) {
print(line, terminator: "\n")
}
}

View File

@ -0,0 +1,21 @@
import Foundation
public class ProgressObserver: NSObject {
@objc var progressToObserve: Progress
var observation: NSKeyValueObservation?
public init(_ progress: Progress) {
progressToObserve = progress
}
func log(_ renderer: Logger) {
renderer.appendNewLine(ProgressObserver.lineToRender(progressToObserve))
observation = observe(\.progressToObserve.fractionCompleted) { progress, _ in
renderer.updateLastLine(ProgressObserver.lineToRender(self.progressToObserve))
}
}
private static func lineToRender(_ progress: Progress) -> String {
String(Int(100 * progress.fractionCompleted)) + "%"
}
}

View File

@ -0,0 +1,17 @@
import Foundation
public class URLSessionLogger: NSObject, URLSessionTaskDelegate {
let renderer: Logger
public init(_ renderer: Logger) {
self.renderer = renderer
}
public func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
renderer.updateLastLine(URLSessionLogger.lineToRender(task.progress))
}
private static func lineToRender(_ progress: Progress) -> String {
String(100 * progress.completedUnitCount / progress.totalUnitCount) + "%"
}
}

View File

@ -3,6 +3,7 @@ import Virtualization
struct UnsupportedRestoreImageError: Error {}
struct NoMainScreenFoundError: Error {}
struct DownloadFailed: Error {}
class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Virtualization.Framework's virtual machine
@ -37,13 +38,42 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
}
static func retrieveLatestIPSW() async throws -> URL {
defaultLogger.appendNewLine("Looking up the latest supported IPSW...")
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.fetchLatestSupported() { result in continuation.resume(with: result) }
}
let (downloadedImageURL, _) = try await URLSession.shared.download(from: image.url, delegate: nil)
let ipswCacheFolder = VMStorage.tartCacheDir.appendingPathComponent("IPSWs", isDirectory: true)
try FileManager.default.createDirectory(at: ipswCacheFolder, withIntermediateDirectories: true)
return downloadedImageURL
let expectedIPSWLocation = ipswCacheFolder.appendingPathComponent("\(image.buildVersion).ipsw", isDirectory: false)
if FileManager.default.fileExists(atPath: expectedIPSWLocation.path) {
defaultLogger.appendNewLine("Using cached *.ipsw file...")
return expectedIPSWLocation
}
defaultLogger.appendNewLine("Fetching \(expectedIPSWLocation.lastPathComponent)...")
let data: Data = try await withCheckedThrowingContinuation { continuation in
let downloadedTask = URLSession.shared.dataTask(with: image.url) { data, response, error in
if error != nil {
continuation.resume(throwing: error!)
return
}
if (data == nil) {
continuation.resume(throwing: DownloadFailed())
return
}
continuation.resume(returning: data!)
}
ProgressObserver(downloadedTask.progress).log(defaultLogger)
downloadedTask.resume()
}
try data.write(to: expectedIPSWLocation, options: [.atomic])
return expectedIPSWLocation
}
init(vmDir: VMDirectory, ipswURL: URL?, diskSize: UInt64 = 32 * 1024 * 1024 * 1024) async throws {
@ -94,6 +124,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.main.async {
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
defaultLogger.appendNewLine("Installing OS...")
ProgressObserver(installer.progress).log(defaultLogger)
installer.install { result in continuation.resume(with: result) }
}

View File

@ -1,15 +1,12 @@
import Foundation
struct VMStorage {
var homeDir: URL
var tartDir: URL
var vmsDir: URL
public static let tartHomeDir: URL = FileManager.default
.homeDirectoryForCurrentUser
.appendingPathComponent(".tart", isDirectory: true)
init() {
homeDir = FileManager.default.homeDirectoryForCurrentUser
tartDir = homeDir.appendingPathComponent(".tart", isDirectory: true)
vmsDir = tartDir.appendingPathComponent("vms", isDirectory: true)
}
public static let tartVMsDir: URL = tartHomeDir.appendingPathComponent("vms", isDirectory: true)
public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
func create(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
@ -33,9 +30,10 @@ struct VMStorage {
func list() throws -> [URL] {
do {
return try FileManager.default.contentsOfDirectory(at: vmsDir,
includingPropertiesForKeys: [.isDirectoryKey],
options: .skipsSubdirectoryDescendants)
return try FileManager.default.contentsOfDirectory(
at: VMStorage.tartVMsDir,
includingPropertiesForKeys: [.isDirectoryKey],
options: .skipsSubdirectoryDescendants)
} catch {
if error.isFileNotFound() {
return []
@ -46,7 +44,10 @@ struct VMStorage {
}
private func vmURL(_ name: String) -> URL {
return URL.init(fileURLWithPath: name, isDirectory: true, relativeTo: vmsDir)
return URL.init(
fileURLWithPath: name,
isDirectory: true,
relativeTo: VMStorage.tartVMsDir)
}
}