Refactored UI (#22)

* Simplified crafting of a VM config

* Configure display

* Propagate VM name to the title
This commit is contained in:
Fedor Korotkov 2022-04-13 16:42:27 -04:00 committed by GitHub
parent 466c3c568a
commit 75b5d63387
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 145 additions and 102 deletions

View File

@ -39,8 +39,20 @@ struct Run: AsyncParsableCommand {
struct MainApp: App {
var body: some Scene {
WindowGroup {
VMView(vm: vm!)
WindowGroup(vm!.name) {
Group {
VMView(vm: vm!).onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
}.commands {
// Remove some standard menu options
CommandGroup(replacing: .help, addition: {})
CommandGroup(replacing: .newItem, addition: {})
CommandGroup(replacing: .pasteboard, addition: {})
CommandGroup(replacing: .textEditing, addition: {})
CommandGroup(replacing: .undoRedo, addition: {})
CommandGroup(replacing: .windowSize, addition: {})
}
}
}
@ -56,7 +68,9 @@ struct VMView: NSViewRepresentable {
@ObservedObject var vm: VM
func makeNSView(context: Context) -> NSViewType {
VZVirtualMachineView()
let machineView = VZVirtualMachineView()
machineView.capturesSystemKeys = true
return machineView
}
func updateNSView(_ nsView: NSViewType, context: Context) {

View File

@ -13,6 +13,9 @@ struct Set: AsyncParsableCommand {
@Option(help: "VM memory size in megabytes")
var memory: UInt16?
@Option(help: "VM display settings in a format of <width>x<height>(x<dpi>)?. For example, 1200x800 or 1200x800x72")
var display: VMDisplayConfig?
func run() async throws {
do {
let vmStorage = VMStorage()
@ -27,6 +30,18 @@ struct Set: AsyncParsableCommand {
try vmConfig.setMemory(memorySize: UInt64(memory) * 1024 * 1024)
}
if let display = display {
if (display.width > 0) {
vmConfig.display.width = display.width
}
if (display.height > 0) {
vmConfig.display.height = display.height
}
if (display.dpi > 0) {
vmConfig.display.dpi = display.dpi
}
}
try vmConfig.save(toURL: vmDir.configURL)
Foundation.exit(0)
@ -37,3 +52,16 @@ struct Set: AsyncParsableCommand {
}
}
}
extension VMDisplayConfig: ExpressibleByArgument {
public init(argument: String) {
let parts = argument.components(separatedBy: "x").map {
Int($0) ?? 0
}
self = VMDisplayConfig(
width: parts[safe: 0] ?? 0,
height: parts[safe: 1] ?? 0,
dpi: parts[safe: 2] ?? 0
)
}
}

7
Sources/tart/Utils.swift Normal file
View File

@ -0,0 +1,7 @@
import Foundation
extension Collection {
subscript (safe index: Index) -> Element? {
indices.contains(index) ? self[index] : nil
}
}

View File

@ -18,36 +18,31 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
var sema = DispatchSemaphore(value: 0)
// VM's config
var vmConfig: VMConfig
var name: String
// VM's config
var config: VMConfig
init(vmDir: VMDirectory) throws {
let auxStorage = VZMacAuxiliaryStorage(contentsOf: vmDir.nvramURL)
self.vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
name = vmDir.name
config = try VMConfig.init(fromURL: vmDir.configURL)
let configuration = try VM.craftConfiguration(
diskURL: vmDir.diskURL,
ecid: vmConfig.ecid,
auxStorage: auxStorage,
hardwareModel: vmConfig.hardwareModel,
cpuCount: vmConfig.cpuCount,
memorySize: vmConfig.memorySize,
macAddress: vmConfig.macAddress
)
self.virtualMachine = VZVirtualMachine(configuration: configuration)
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
self.virtualMachine.delegate = self
virtualMachine.delegate = self
}
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)
}
continuation.resume(with: result)
}
}
@ -65,16 +60,16 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
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!)
}
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()
}
@ -90,8 +85,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// that match both the image and our platform
let image = try await withCheckedThrowingContinuation { continuation in
VZMacOSRestoreImage.load(from: ipswURL) { result in
continuation.resume(with: result)
}
continuation.resume(with: result)
}
}
guard let requirements = image.mostFeaturefulSupportedConfiguration else {
@ -107,91 +102,77 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
try diskFileHandle.truncate(atOffset: diskSize)
try diskFileHandle.close()
name = vmDir.name
// Create config
self.vmConfig = VMConfig(
config = VMConfig(
hardwareModel: requirements.hardwareModel,
cpuCountMin: requirements.minimumSupportedCPUCount,
memorySizeMin: requirements.minimumSupportedMemorySize
)
try self.vmConfig.save(toURL: vmDir.configURL)
try config.save(toURL: vmDir.configURL)
// Initialize the virtual machine and its configuration
let configuration = try VM.craftConfiguration(
diskURL: vmDir.diskURL,
ecid: self.vmConfig.ecid,
auxStorage: auxStorage,
hardwareModel: requirements.hardwareModel,
cpuCount: self.vmConfig.cpuCount,
memorySize: self.vmConfig.memorySize,
macAddress: self.vmConfig.macAddress
)
self.virtualMachine = VZVirtualMachine(configuration: configuration)
let configuration = try VM.craftConfiguration(diskURL: vmDir.diskURL, auxStorage: auxStorage, vmConfig: config)
virtualMachine = VZVirtualMachine(configuration: configuration)
super.init()
self.virtualMachine.delegate = self
virtualMachine.delegate = self
// Run automated installation
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
DispatchQueue.main.async {
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
let installer = VZMacOSInstaller(virtualMachine: self.virtualMachine, restoringFromImageAt: ipswURL)
defaultLogger.appendNewLine("Installing OS...")
ProgressObserver(installer.progress).log(defaultLogger)
defaultLogger.appendNewLine("Installing OS...")
ProgressObserver(installer.progress).log(defaultLogger)
installer.install { result in
continuation.resume(with: result)
}
}
installer.install { result in
continuation.resume(with: result)
}
}
}
}
func run() async throws {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.main.async {
self.virtualMachine.start(completionHandler: { result in
continuation.resume(with: result)
})
}
self.virtualMachine.start(completionHandler: { result in
continuation.resume(with: result)
})
}
}
sema.wait()
}
static func craftConfiguration(
diskURL: URL,
ecid: VZMacMachineIdentifier,
auxStorage: VZMacAuxiliaryStorage,
hardwareModel: VZMacHardwareModel,
cpuCount: Int,
memorySize: UInt64,
macAddress: VZMACAddress
) throws -> VZVirtualMachineConfiguration {
static func craftConfiguration(diskURL: URL, auxStorage: VZMacAuxiliaryStorage, vmConfig: VMConfig) throws -> VZVirtualMachineConfiguration {
let configuration = VZVirtualMachineConfiguration()
// Boot loader
configuration.bootLoader = VZMacOSBootLoader()
// CPU and memory
configuration.cpuCount = cpuCount
configuration.memorySize = memorySize
configuration.cpuCount = vmConfig.cpuCount
configuration.memorySize = vmConfig.memorySize
// Platform
let platform = VZMacPlatformConfiguration()
platform.machineIdentifier = ecid
platform.machineIdentifier = vmConfig.ecid
platform.auxiliaryStorage = auxStorage
platform.hardwareModel = hardwareModel
platform.hardwareModel = vmConfig.hardwareModel
configuration.platform = platform
// Display
let graphicsDeviceConfiguration = VZMacGraphicsDeviceConfiguration()
guard let mainScreen = NSScreen.main else {
throw NoMainScreenFoundError()
}
graphicsDeviceConfiguration.displays = [
VZMacGraphicsDisplayConfiguration(for: mainScreen, sizeInPoints: mainScreen.frame.size)
VZMacGraphicsDisplayConfiguration(
widthInPixels: vmConfig.display.width,
heightInPixels: vmConfig.display.height,
pixelsPerInch: vmConfig.display.dpi
)
]
configuration.graphicsDevices = [graphicsDeviceConfiguration]
@ -202,7 +183,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Networking
let vio = VZVirtioNetworkDeviceConfiguration()
vio.attachment = VZNATNetworkDeviceAttachment()
vio.macAddress = macAddress
vio.macAddress = vmConfig.macAddress
configuration.networkDevices = [vio]
// Storage

View File

@ -9,7 +9,7 @@ class LessThanMinimalResourcesError: NSObject, LocalizedError {
override var description: String {
get {
return "LessThanMinimalResourcesError: \(self.userExplanation)"
"LessThanMinimalResourcesError: \(userExplanation)"
}
}
}
@ -23,9 +23,16 @@ enum CodingKeys: String, CodingKey {
case memorySizeMin
case memorySize
case macAddress
case display
}
struct VMConfig: Encodable, Decodable {
struct VMDisplayConfig: Codable {
var width: Int = 1024
var height: Int = 768
var dpi: Int = 72
}
struct VMConfig: Codable {
var version: Int = 1
var ecid: VZMacMachineIdentifier
var hardwareModel: VZMacHardwareModel
@ -34,6 +41,8 @@ struct VMConfig: Encodable, Decodable {
var memorySizeMin: UInt64
private(set) var memorySize: UInt64
var macAddress: VZMACAddress
var display: VMDisplayConfig = VMDisplayConfig()
init(
ecid: VZMacMachineIdentifier = VZMacMachineIdentifier(),
@ -44,11 +53,11 @@ struct VMConfig: Encodable, Decodable {
) {
self.ecid = ecid
self.hardwareModel = hardwareModel
self.cpuCountMin = cpuCountMin
self.cpuCount = cpuCountMin
self.memorySizeMin = memorySizeMin
self.memorySize = memorySizeMin
self.macAddress = macAddress
self.cpuCountMin = cpuCountMin
self.memorySizeMin = memorySizeMin
cpuCount = cpuCountMin
memorySize = memorySizeMin
}
init(fromURL: URL) throws {
@ -65,7 +74,7 @@ struct VMConfig: Encodable, Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.version = try container.decode(Int.self, forKey: .version)
version = try container.decode(Int.self, forKey: .version)
let encodedECID = try container.decode(String.self, forKey: .ecid)
guard let data = Data.init(base64Encoded: encodedECID) else {
@ -89,10 +98,10 @@ struct VMConfig: Encodable, Decodable {
}
self.hardwareModel = hardwareModel
self.cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin)
self.cpuCount = try container.decode(Int.self, forKey: .cpuCount)
self.memorySizeMin = try container.decode(UInt64.self, forKey: .memorySizeMin)
self.memorySize = try container.decode(UInt64.self, forKey: .memorySize)
cpuCountMin = try container.decode(Int.self, forKey: .cpuCountMin)
cpuCount = try container.decode(Int.self, forKey: .cpuCount)
memorySizeMin = try container.decode(UInt64.self, forKey: .memorySizeMin)
memorySize = try container.decode(UInt64.self, forKey: .memorySize)
let encodedMacAddress = try container.decode(String.self, forKey: .macAddress)
guard let macAddress = VZMACAddress.init(string: encodedMacAddress) else {
@ -102,24 +111,27 @@ struct VMConfig: Encodable, Decodable {
debugDescription: "failed to initialize VZMacAddress using the provided value")
}
self.macAddress = macAddress
display = try container.decodeIfPresent(VMDisplayConfig.self, forKey: .display) ?? VMDisplayConfig()
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.version, forKey: .version)
try container.encode(self.ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
try container.encode(self.hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
try container.encode(self.cpuCountMin, forKey: .cpuCountMin)
try container.encode(self.cpuCount, forKey: .cpuCount)
try container.encode(self.memorySizeMin, forKey: .memorySizeMin)
try container.encode(self.memorySize, forKey: .memorySize)
try container.encode(self.macAddress.string, forKey: .macAddress)
try container.encode(version, forKey: .version)
try container.encode(ecid.dataRepresentation.base64EncodedString(), forKey: .ecid)
try container.encode(hardwareModel.dataRepresentation.base64EncodedString(), forKey: .hardwareModel)
try container.encode(cpuCountMin, forKey: .cpuCountMin)
try container.encode(cpuCount, forKey: .cpuCount)
try container.encode(memorySizeMin, forKey: .memorySizeMin)
try container.encode(memorySize, forKey: .memorySize)
try container.encode(macAddress.string, forKey: .macAddress)
try container.encode(display, forKey: .display)
}
mutating func setCPU(cpuCount: Int) throws {
if cpuCount < self.cpuCountMin {
throw LessThanMinimalResourcesError("VM should have \(self.cpuCountMin) CPU cores"
if cpuCount < cpuCountMin {
throw LessThanMinimalResourcesError("VM should have \(cpuCountMin) CPU cores"
+ " at minimum (requested \(cpuCount))")
}
@ -127,9 +139,9 @@ struct VMConfig: Encodable, Decodable {
}
mutating func setMemory(memorySize: UInt64) throws {
if memorySize < self.memorySizeMin {
throw LessThanMinimalResourcesError("VM should have \(self.memorySizeMin) bytes"
+ " of memory at minimum (requested \(self.memorySizeMin))")
if memorySize < memorySizeMin {
throw LessThanMinimalResourcesError("VM should have \(memorySizeMin) bytes"
+ " of memory at minimum (requested \(memorySizeMin))")
}
self.memorySize = memorySize

View File

@ -7,16 +7,17 @@ struct AlreadyInitializedVMDirectoryError: Error {
}
struct VMDirectory {
var name: String
var baseURL: URL
var configURL: URL {
self.baseURL.appendingPathComponent("config.json")
baseURL.appendingPathComponent("config.json")
}
var diskURL: URL {
self.baseURL.appendingPathComponent("disk.bin")
baseURL.appendingPathComponent("disk.bin")
}
var nvramURL: URL {
self.baseURL.appendingPathComponent("nvram.bin")
baseURL.appendingPathComponent("nvram.bin")
}
var initialized: Bool {

View File

@ -9,7 +9,7 @@ struct VMStorage {
public static let tartCacheDir: URL = tartHomeDir.appendingPathComponent("cache", isDirectory: true)
func create(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
let vmDir = VMDirectory(name: name, baseURL: vmURL(name))
try vmDir.initialize()
@ -17,7 +17,7 @@ struct VMStorage {
}
func read(_ name: String) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
let vmDir = VMDirectory(name: name, baseURL: vmURL(name))
try vmDir.validate()