mirror of https://github.com/cirruslabs/tart.git
Logger (#11)
* Log progress * ProgressObserver * Reverted run configuration * Cache *.ipsw files * Use fraction * Use datatask and pre-create cache folder
This commit is contained in:
parent
483627904c
commit
dc1e502404
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) + "%"
|
||||
}
|
||||
}
|
||||
|
|
@ -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) + "%"
|
||||
}
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue