From dc1e5024045c262fbbe71bb9132ded7cb3603399 Mon Sep 17 00:00:00 2001 From: Fedor Korotkov Date: Tue, 22 Mar 2022 22:17:23 -0400 Subject: [PATCH] Logger (#11) * Log progress * ProgressObserver * Reverted run configuration * Cache *.ipsw files * Use fraction * Use datatask and pre-create cache folder --- Sources/tart/Logging/Logger.swift | 46 +++++++++++++++++++++ Sources/tart/Logging/ProgressObserver.swift | 21 ++++++++++ Sources/tart/Logging/URLSessionLogger.swift | 17 ++++++++ Sources/tart/VM.swift | 37 ++++++++++++++++- Sources/tart/VMStorage.swift | 25 +++++------ 5 files changed, 132 insertions(+), 14 deletions(-) create mode 100644 Sources/tart/Logging/Logger.swift create mode 100644 Sources/tart/Logging/ProgressObserver.swift create mode 100644 Sources/tart/Logging/URLSessionLogger.swift diff --git a/Sources/tart/Logging/Logger.swift b/Sources/tart/Logging/Logger.swift new file mode 100644 index 0000000..a14a2ee --- /dev/null +++ b/Sources/tart/Logging/Logger.swift @@ -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") + } +} diff --git a/Sources/tart/Logging/ProgressObserver.swift b/Sources/tart/Logging/ProgressObserver.swift new file mode 100644 index 0000000..729bf70 --- /dev/null +++ b/Sources/tart/Logging/ProgressObserver.swift @@ -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)) + "%" + } +} diff --git a/Sources/tart/Logging/URLSessionLogger.swift b/Sources/tart/Logging/URLSessionLogger.swift new file mode 100644 index 0000000..240d316 --- /dev/null +++ b/Sources/tart/Logging/URLSessionLogger.swift @@ -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) + "%" + } +} diff --git a/Sources/tart/VM.swift b/Sources/tart/VM.swift index dab053a..81b68aa 100644 --- a/Sources/tart/VM.swift +++ b/Sources/tart/VM.swift @@ -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) 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) } } diff --git a/Sources/tart/VMStorage.swift b/Sources/tart/VMStorage.swift index d11d19c..2e3b658 100644 --- a/Sources/tart/VMStorage.swift +++ b/Sources/tart/VMStorage.swift @@ -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) } }