|
@@ -0,0 +1,607 @@
|
|
|
+//
|
|
|
+// Copyright © 2023 osy. All rights reserved.
|
|
|
+//
|
|
|
+// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+// you may not use this file except in compliance with the License.
|
|
|
+// You may obtain a copy of the License at
|
|
|
+//
|
|
|
+// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+//
|
|
|
+// Unless required by applicable law or agreed to in writing, software
|
|
|
+// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+// See the License for the specific language governing permissions and
|
|
|
+// limitations under the License.
|
|
|
+//
|
|
|
+
|
|
|
+import Foundation
|
|
|
+
|
|
|
+@objc extension UTMScriptingVirtualMachineImpl {
|
|
|
+ @objc var configuration: [AnyHashable : Any] {
|
|
|
+ if let vm = vm as? UTMQemuVirtualMachine {
|
|
|
+ return serializeQemuConfiguration(vm.qemuConfig)
|
|
|
+ } else if let vm = vm as? UTMAppleVirtualMachine {
|
|
|
+ return serializeAppleConfiguration(vm.appleConfig)
|
|
|
+ } else {
|
|
|
+ fatalError()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func updateConfiguration(_ command: NSScriptCommand) {
|
|
|
+ let newConfiguration = command.evaluatedArguments?["newConfiguration"] as? [AnyHashable : Any]
|
|
|
+ withScriptCommand(command) { [self] in
|
|
|
+ guard let newConfiguration = newConfiguration else {
|
|
|
+ throw ScriptingError.invalidParameter
|
|
|
+ }
|
|
|
+ guard vm.state == .vmStopped else {
|
|
|
+ throw ScriptingError.notStopped
|
|
|
+ }
|
|
|
+ if backend == .qemu {
|
|
|
+ try updateQemuConfiguration(from: newConfiguration)
|
|
|
+ } else if backend == .apple {
|
|
|
+ try updateAppleConfiguration(from: newConfiguration)
|
|
|
+ } else {
|
|
|
+ fatalError()
|
|
|
+ }
|
|
|
+ try await data.save(vm: vm)
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@MainActor
|
|
|
+extension UTMScriptingVirtualMachineImpl {
|
|
|
+ private var bytesInMib: Int64 {
|
|
|
+ 1048576
|
|
|
+ }
|
|
|
+
|
|
|
+ private func qemuDirectoryShareMode(from mode: QEMUFileShareMode) -> UTMScriptingQemuDirectoryShareMode {
|
|
|
+ switch mode {
|
|
|
+ case .none: return .none
|
|
|
+ case .webdav: return .webDAV
|
|
|
+ case .virtfs: return .virtFS
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeQemuConfiguration(_ config: UTMQemuConfiguration) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "name": config.information.name,
|
|
|
+ "notes": config.information.notes ?? "",
|
|
|
+ "architecture": config.system.architecture.rawValue,
|
|
|
+ "machine": config.system.target.rawValue,
|
|
|
+ "memory": config.system.memorySize,
|
|
|
+ "cpuCores": config.system.cpuCount,
|
|
|
+ "hypervisor": config.qemu.hasHypervisor,
|
|
|
+ "uefi": config.qemu.hasUefiBoot,
|
|
|
+ "directoryShareMode": qemuDirectoryShareMode(from: config.sharing.directoryShareMode).rawValue,
|
|
|
+ "drives": config.drives.map({ serializeQemuDriveExisting($0) }),
|
|
|
+ "networkInterfaces": config.networks.enumerated().map({ serializeQemuNetwork($1, index: $0) }),
|
|
|
+ "serialPorts": config.serials.enumerated().map({ serializeQemuSerial($1, index: $0) }),
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func size(of drive: any UTMConfigurationDrive) -> Int {
|
|
|
+ guard let url = drive.imageURL else {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return Int(data.computeSize(for: url) / bytesInMib)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func qemuDriveInterface(from interface: QEMUDriveInterface) -> UTMScriptingQemuDriveInterface {
|
|
|
+ switch interface {
|
|
|
+ case .none: return .none
|
|
|
+ case .ide: return .ide
|
|
|
+ case .scsi: return .scsi
|
|
|
+ case .sd: return .sd
|
|
|
+ case .mtd: return .mtd
|
|
|
+ case .floppy: return .floppy
|
|
|
+ case .pflash: return .pFlash
|
|
|
+ case .virtio: return .virtIO
|
|
|
+ case .nvme: return .nvMe
|
|
|
+ case .usb: return .usb
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeQemuDriveExisting(_ config: UTMQemuConfigurationDrive) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "id": config.id,
|
|
|
+ "removable": config.isExternal,
|
|
|
+ "interface": qemuDriveInterface(from: config.interface).rawValue,
|
|
|
+ "hostSize": size(of: config),
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func qemuNetworkMode(from mode: QEMUNetworkMode) -> UTMScriptingQemuNetworkMode {
|
|
|
+ switch mode {
|
|
|
+ case .emulated: return .emulated
|
|
|
+ case .shared: return .shared
|
|
|
+ case .host: return .host
|
|
|
+ case .bridged: return .bridged
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeQemuNetwork(_ config: UTMQemuConfigurationNetwork, index: Int) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "index": index,
|
|
|
+ "hardware": config.hardware.rawValue,
|
|
|
+ "mode": qemuNetworkMode(from: config.mode).rawValue,
|
|
|
+ "address": config.macAddress,
|
|
|
+ "hostInterface": config.bridgeInterface ?? "",
|
|
|
+ "portForwards": config.portForward.map({ serializeQemuPortForward($0) }),
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func networkProtocol(from protc: QEMUNetworkProtocol) -> UTMScriptingNetworkProtocol {
|
|
|
+ switch protc {
|
|
|
+ case .tcp: return .tcp
|
|
|
+ case .udp: return .udp
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeQemuPortForward(_ config: UTMQemuConfigurationPortForward) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "protocol": networkProtocol(from: config.protocol).rawValue,
|
|
|
+ "hostAddress": config.hostAddress ?? "",
|
|
|
+ "hostPort": config.hostPort,
|
|
|
+ "guestAddress": config.guestAddress ?? "",
|
|
|
+ "guestPort": config.guestPort,
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func qemuSerialInterface(from mode: QEMUSerialMode) -> UTMScriptingSerialInterface {
|
|
|
+ switch mode {
|
|
|
+ case .ptty: return .ptty
|
|
|
+ case .tcpServer: return .tcp
|
|
|
+ default: return .unavailable
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeQemuSerial(_ config: UTMQemuConfigurationSerial, index: Int) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "index": index,
|
|
|
+ "hardware": config.hardware?.rawValue ?? "",
|
|
|
+ "interface": qemuSerialInterface(from: config.mode).rawValue,
|
|
|
+ "port": config.tcpPort ?? 0,
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeAppleConfiguration(_ config: UTMAppleConfiguration) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "name": config.information.name,
|
|
|
+ "notes": config.information.notes ?? "",
|
|
|
+ "memory": config.system.memorySize,
|
|
|
+ "cpuCores": config.system.cpuCount,
|
|
|
+ "directoryShares": config.sharedDirectories.enumerated().map({ serializeAppleDirectoryShare($1, index: $0) }),
|
|
|
+ "drives": config.drives.map({ serializeAppleDriveExisting($0) }),
|
|
|
+ "networkInterfaces": config.networks.enumerated().map({ serializeAppleNetwork($1, index: $0) }),
|
|
|
+ "serialPorts": config.serials.enumerated().map({ serializeAppleSerial($1, index: $0) }),
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeAppleDirectoryShare(_ config: UTMAppleConfigurationSharedDirectory, index: Int) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "index": index,
|
|
|
+ "readOnly": config.isReadOnly
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeAppleDriveExisting(_ config: UTMAppleConfigurationDrive) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "id": config.id,
|
|
|
+ "removable": config.isExternal,
|
|
|
+ "hostSize": size(of: config),
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func appleNetworkMode(from mode: UTMAppleConfigurationNetwork.NetworkMode) -> UTMScriptingAppleNetworkMode {
|
|
|
+ switch mode {
|
|
|
+ case .shared: return .shared
|
|
|
+ case .bridged: return .bridged
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeAppleNetwork(_ config: UTMAppleConfigurationNetwork, index: Int) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "index": index,
|
|
|
+ "mode": appleNetworkMode(from: config.mode).rawValue,
|
|
|
+ "address": config.macAddress,
|
|
|
+ "hostInterface": config.bridgeInterface ?? "",
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ private func appleSerialInterface(from mode: UTMAppleConfigurationSerial.SerialMode) -> UTMScriptingSerialInterface {
|
|
|
+ switch mode {
|
|
|
+ case .ptty: return .ptty
|
|
|
+ default: return .unavailable
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func serializeAppleSerial(_ config: UTMAppleConfigurationSerial, index: Int) -> [AnyHashable : Any] {
|
|
|
+ [
|
|
|
+ "index": index,
|
|
|
+ "interface": appleSerialInterface(from: config.mode).rawValue,
|
|
|
+ ]
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@MainActor
|
|
|
+extension UTMScriptingVirtualMachineImpl {
|
|
|
+ private func updateElements<T>(_ array: inout [T], with records: [[AnyHashable : Any]], onExisting: @MainActor (inout T, [AnyHashable : Any]) throws -> Void, onNew: @MainActor ([AnyHashable : Any]) throws -> T) throws {
|
|
|
+ var unseenIndicies = IndexSet(integersIn: array.indices)
|
|
|
+ for record in records {
|
|
|
+ if let index = record["index"] as? Int {
|
|
|
+ guard array.indices.contains(index) else {
|
|
|
+ throw ConfigurationError.indexNotFound(index: index)
|
|
|
+ }
|
|
|
+ try onExisting(&array[index], record)
|
|
|
+ unseenIndicies.remove(index)
|
|
|
+ } else {
|
|
|
+ array.append(try onNew(record))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ array.remove(atOffsets: unseenIndicies)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateIdentifiedElements<T: Identifiable>(_ array: inout [T], with records: [[AnyHashable : Any]], onExisting: @MainActor (inout T, [AnyHashable : Any]) throws -> Void, onNew: @MainActor ([AnyHashable : Any]) throws -> T) throws {
|
|
|
+ var unseenIndicies = IndexSet(integersIn: array.indices)
|
|
|
+ for record in records {
|
|
|
+ if let id = record["id"] as? T.ID {
|
|
|
+ guard let index = array.enumerated().first(where: { $1.id == id })?.offset else {
|
|
|
+ throw ConfigurationError.identifierNotFound(id: id)
|
|
|
+ }
|
|
|
+ try onExisting(&array[index], record)
|
|
|
+ unseenIndicies.remove(index)
|
|
|
+ } else {
|
|
|
+ array.append(try onNew(record))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ array.remove(atOffsets: unseenIndicies)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseQemuDirectoryShareMode(_ value: AEKeyword?) -> QEMUFileShareMode? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingQemuDirectoryShareMode(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .none: return QEMUFileShareMode.none
|
|
|
+ case .webDAV: return .webdav
|
|
|
+ case .virtFS: return .virtfs
|
|
|
+ default: return Optional.none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuConfiguration(from record: [AnyHashable : Any]) throws {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ if let name = record["name"] as? String, !name.isEmpty {
|
|
|
+ config.information.name = name
|
|
|
+ }
|
|
|
+ if let notes = record["notes"] as? String, !notes.isEmpty {
|
|
|
+ config.information.notes = notes
|
|
|
+ }
|
|
|
+ let architecture = record["architecture"] as? String
|
|
|
+ let arch = QEMUArchitecture(rawValue: architecture ?? "")
|
|
|
+ let machine = record["machine"] as? String
|
|
|
+ let target = arch?.targetType.init(rawValue: machine ?? "")
|
|
|
+ if let arch = arch, arch != config.system.architecture {
|
|
|
+ let target = target ?? arch.targetType.default
|
|
|
+ config.system.architecture = arch
|
|
|
+ config.system.target = target
|
|
|
+ config.reset(forArchitecture: arch, target: target)
|
|
|
+ } else if let target = target {
|
|
|
+ config.system.target = target
|
|
|
+ config.reset(forArchitecture: config.system.architecture, target: target)
|
|
|
+ }
|
|
|
+ if let memory = record["memory"] as? Int, memory != 0 {
|
|
|
+ config.system.memorySize = memory
|
|
|
+ }
|
|
|
+ if let cpuCores = record["cpuCores"] as? Int {
|
|
|
+ config.system.cpuCount = cpuCores
|
|
|
+ }
|
|
|
+ if let hypervisor = record["hypervisor"] as? Bool {
|
|
|
+ config.qemu.hasHypervisor = hypervisor
|
|
|
+ }
|
|
|
+ if let uefi = record["uefi"] as? Bool {
|
|
|
+ config.qemu.hasUefiBoot = uefi
|
|
|
+ }
|
|
|
+ if let directoryShareMode = parseQemuDirectoryShareMode(record["directoryShareMode"] as? AEKeyword) {
|
|
|
+ config.sharing.directoryShareMode = directoryShareMode
|
|
|
+ }
|
|
|
+ if let drives = record["drives"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateQemuDrives(from: drives)
|
|
|
+ }
|
|
|
+ if let networkInterfaces = record["networkInterfaces"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateQemuNetworks(from: networkInterfaces)
|
|
|
+ }
|
|
|
+ if let serialPorts = record["serialPorts"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateQemuSerials(from: serialPorts)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseQemuDriveInterface(_ value: AEKeyword?) -> QEMUDriveInterface? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingQemuDriveInterface(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .none: return QEMUDriveInterface.none
|
|
|
+ case .ide: return .ide
|
|
|
+ case .scsi: return .scsi
|
|
|
+ case .sd: return .sd
|
|
|
+ case .mtd: return .mtd
|
|
|
+ case .floppy: return .floppy
|
|
|
+ case .pFlash: return .pflash
|
|
|
+ case .virtIO: return .virtio
|
|
|
+ case .nvMe: return .nvme
|
|
|
+ case .usb: return .usb
|
|
|
+ default: return Optional.none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuDrives(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ try updateIdentifiedElements(&config.drives, with: records, onExisting: updateQemuExistingDrive, onNew: unserializeQemuDriveNew)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuExistingDrive(_ drive: inout UTMQemuConfigurationDrive, from record: [AnyHashable : Any]) throws {
|
|
|
+ if let interface = parseQemuDriveInterface(record["interface"] as? AEKeyword) {
|
|
|
+ drive.interface = interface
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func unserializeQemuDriveNew(from record: [AnyHashable : Any]) throws -> UTMQemuConfigurationDrive {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ let removable = record["removable"] as? Bool ?? false
|
|
|
+ var newDrive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: removable)
|
|
|
+ if let importUrl = record["source"] as? URL {
|
|
|
+ newDrive.imageURL = importUrl
|
|
|
+ } else if let size = record["guestSize"] as? Int {
|
|
|
+ newDrive.sizeMib = size
|
|
|
+ }
|
|
|
+ if let interface = parseQemuDriveInterface(record["interface"] as? AEKeyword) {
|
|
|
+ newDrive.interface = interface
|
|
|
+ }
|
|
|
+ if let raw = record["raw"] as? Bool {
|
|
|
+ newDrive.isRawImage = raw
|
|
|
+ }
|
|
|
+ return newDrive
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuNetworks(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ try updateElements(&config.networks, with: records, onExisting: updateQemuExistingNetwork, onNew: { record in
|
|
|
+ guard var newNetwork = UTMQemuConfigurationNetwork(forArchitecture: config.system.architecture, target: config.system.target) else {
|
|
|
+ throw ConfigurationError.deviceNotSupported
|
|
|
+ }
|
|
|
+ try updateQemuExistingNetwork(&newNetwork, from: record)
|
|
|
+ return newNetwork
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseQemuNetworkMode(_ value: AEKeyword?) -> QEMUNetworkMode? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingQemuNetworkMode(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .emulated: return .emulated
|
|
|
+ case .shared: return .shared
|
|
|
+ case .host: return .host
|
|
|
+ case .bridged: return .bridged
|
|
|
+ default: return .none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuExistingNetwork(_ network: inout UTMQemuConfigurationNetwork, from record: [AnyHashable : Any]) throws {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.networkDeviceType.init(rawValue: hardware) {
|
|
|
+ network.hardware = hardware
|
|
|
+ }
|
|
|
+ if let mode = parseQemuNetworkMode(record["mode"] as? AEKeyword) {
|
|
|
+ network.mode = mode
|
|
|
+ }
|
|
|
+ if let address = record["address"] as? String, !address.isEmpty {
|
|
|
+ network.macAddress = address
|
|
|
+ }
|
|
|
+ if let interface = record["hostInterface"] as? String, !interface.isEmpty {
|
|
|
+ network.bridgeInterface = interface
|
|
|
+ }
|
|
|
+ if let portForwards = record["portForwards"] as? [[AnyHashable : Any]] {
|
|
|
+ network.portForward = portForwards.map({ unserializeQemuPortForward(from: $0) })
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseNetworkProtocol(_ value: AEKeyword?) -> QEMUNetworkProtocol? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingNetworkProtocol(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .tcp: return .tcp
|
|
|
+ case .udp: return .udp
|
|
|
+ default: return Optional.none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func unserializeQemuPortForward(from record: [AnyHashable : Any]) -> UTMQemuConfigurationPortForward {
|
|
|
+ var forward = UTMQemuConfigurationPortForward()
|
|
|
+ if let protoc = parseNetworkProtocol(record["protocol"] as? AEKeyword) {
|
|
|
+ forward.protocol = protoc
|
|
|
+ }
|
|
|
+ if let hostAddress = record["hostAddress"] as? String, !hostAddress.isEmpty {
|
|
|
+ forward.hostAddress = hostAddress
|
|
|
+ }
|
|
|
+ if let hostPort = record["hostPort"] as? Int {
|
|
|
+ forward.hostPort = hostPort
|
|
|
+ }
|
|
|
+ if let guestAddress = record["guestAddress"] as? String, !guestAddress.isEmpty {
|
|
|
+ forward.guestAddress = guestAddress
|
|
|
+ }
|
|
|
+ if let guestPort = record["guestPort"] as? Int {
|
|
|
+ forward.guestPort = guestPort
|
|
|
+ }
|
|
|
+ return forward
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuSerials(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ try updateElements(&config.serials, with: records, onExisting: updateQemuExistingSerial, onNew: { record in
|
|
|
+ guard var newSerial = UTMQemuConfigurationSerial(forArchitecture: config.system.architecture, target: config.system.target) else {
|
|
|
+ throw ConfigurationError.deviceNotSupported
|
|
|
+ }
|
|
|
+ try updateQemuExistingSerial(&newSerial, from: record)
|
|
|
+ return newSerial
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseQemuSerialInterface(_ value: AEKeyword?) -> QEMUSerialMode? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingSerialInterface(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .ptty: return .ptty
|
|
|
+ case .tcp: return .tcpServer
|
|
|
+ default: return Optional.none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateQemuExistingSerial(_ serial: inout UTMQemuConfigurationSerial, from record: [AnyHashable : Any]) throws {
|
|
|
+ let config = (vm as! UTMQemuVirtualMachine).qemuConfig
|
|
|
+ if let hardware = record["hardware"] as? String, let hardware = config.system.architecture.serialDeviceType.init(rawValue: hardware) {
|
|
|
+ serial.hardware = hardware
|
|
|
+ }
|
|
|
+ if let interface = parseQemuSerialInterface(record["interface"] as? AEKeyword) {
|
|
|
+ serial.mode = interface
|
|
|
+ }
|
|
|
+ if let port = record["port"] as? Int {
|
|
|
+ serial.tcpPort = port
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleConfiguration(from record: [AnyHashable : Any]) throws {
|
|
|
+ let config = (vm as! UTMAppleVirtualMachine).appleConfig
|
|
|
+ if let name = record["name"] as? String, !name.isEmpty {
|
|
|
+ config.information.name = name
|
|
|
+ }
|
|
|
+ if let notes = record["notes"] as? String, !notes.isEmpty {
|
|
|
+ config.information.notes = notes
|
|
|
+ }
|
|
|
+ if let memory = record["memory"] as? Int, memory != 0 {
|
|
|
+ config.system.memorySize = memory
|
|
|
+ }
|
|
|
+ if let cpuCores = record["cpuCores"] as? Int {
|
|
|
+ config.system.cpuCount = cpuCores
|
|
|
+ }
|
|
|
+ if let directoryShares = record["directoryShares"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateAppleDirectoryShares(from: directoryShares)
|
|
|
+ }
|
|
|
+ if let drives = record["drives"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateAppleDrives(from: drives)
|
|
|
+ }
|
|
|
+ if let networkInterfaces = record["networkInterfaces"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateAppleNetworks(from: networkInterfaces)
|
|
|
+ }
|
|
|
+ if let serialPorts = record["serialPorts"] as? [[AnyHashable : Any]] {
|
|
|
+ try updateAppleSerials(from: serialPorts)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleDirectoryShares(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMAppleVirtualMachine).appleConfig
|
|
|
+ try updateElements(&config.sharedDirectories, with: records, onExisting: updateAppleExistingDirectoryShare, onNew: { record in
|
|
|
+ var newShare = UTMAppleConfigurationSharedDirectory(directoryURL: nil, isReadOnly: false)
|
|
|
+ try updateAppleExistingDirectoryShare(&newShare, from: record)
|
|
|
+ return newShare
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleExistingDirectoryShare(_ share: inout UTMAppleConfigurationSharedDirectory, from record: [AnyHashable : Any]) throws {
|
|
|
+ if let readOnly = record["readOnly"] as? Bool {
|
|
|
+ share.isReadOnly = readOnly
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleDrives(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMAppleVirtualMachine).appleConfig
|
|
|
+ try updateIdentifiedElements(&config.drives, with: records, onExisting: { _, _ in }, onNew: unserializeAppleNewDrive)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func unserializeAppleNewDrive(from record: [AnyHashable : Any]) throws -> UTMAppleConfigurationDrive {
|
|
|
+ let removable = record["removable"] as? Bool ?? false
|
|
|
+ var newDrive: UTMAppleConfigurationDrive
|
|
|
+ if let size = record["guestSize"] as? Int {
|
|
|
+ newDrive = UTMAppleConfigurationDrive(newSize: size)
|
|
|
+ } else {
|
|
|
+ newDrive = UTMAppleConfigurationDrive(existingURL: record["source"] as? URL, isExternal: removable)
|
|
|
+ }
|
|
|
+ return newDrive
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleNetworks(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMAppleVirtualMachine).appleConfig
|
|
|
+ try updateElements(&config.networks, with: records, onExisting: updateAppleExistingNetwork, onNew: { record in
|
|
|
+ var newNetwork = UTMAppleConfigurationNetwork()
|
|
|
+ try updateAppleExistingNetwork(&newNetwork, from: record)
|
|
|
+ return newNetwork
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseAppleNetworkMode(_ value: AEKeyword?) -> UTMAppleConfigurationNetwork.NetworkMode? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingQemuNetworkMode(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .shared: return .shared
|
|
|
+ case .bridged: return .bridged
|
|
|
+ default: return Optional.none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleExistingNetwork(_ network: inout UTMAppleConfigurationNetwork, from record: [AnyHashable : Any]) throws {
|
|
|
+ if let mode = parseAppleNetworkMode(record["mode"] as? AEKeyword) {
|
|
|
+ network.mode = mode
|
|
|
+ }
|
|
|
+ if let address = record["address"] as? String, !address.isEmpty {
|
|
|
+ network.macAddress = address
|
|
|
+ }
|
|
|
+ if let interface = record["hostInterface"] as? String, !interface.isEmpty {
|
|
|
+ network.bridgeInterface = interface
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleSerials(from records: [[AnyHashable : Any]]) throws {
|
|
|
+ let config = (vm as! UTMAppleVirtualMachine).appleConfig
|
|
|
+ try updateElements(&config.serials, with: records, onExisting: updateAppleExistingSerial, onNew: { record in
|
|
|
+ var newSerial = UTMAppleConfigurationSerial()
|
|
|
+ try updateAppleExistingSerial(&newSerial, from: record)
|
|
|
+ return newSerial
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ private func parseAppleSerialInterface(_ value: AEKeyword?) -> UTMAppleConfigurationSerial.SerialMode? {
|
|
|
+ guard let value = value, let parsed = UTMScriptingSerialInterface(rawValue: value) else {
|
|
|
+ return Optional.none
|
|
|
+ }
|
|
|
+ switch parsed {
|
|
|
+ case .ptty: return .ptty
|
|
|
+ default: return Optional.none
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private func updateAppleExistingSerial(_ serial: inout UTMAppleConfigurationSerial, from record: [AnyHashable : Any]) throws {
|
|
|
+ if let interface = parseAppleSerialInterface(record["interface"] as? AEKeyword) {
|
|
|
+ serial.mode = interface
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ enum ConfigurationError: Error, LocalizedError {
|
|
|
+ case identifierNotFound(id: any Hashable)
|
|
|
+ case invalidDriveDescription
|
|
|
+ case indexNotFound(index: Int)
|
|
|
+ case deviceNotSupported
|
|
|
+
|
|
|
+ var errorDescription: String? {
|
|
|
+ switch self {
|
|
|
+ case .identifierNotFound(let id): return NSLocalizedString("Identifier '\(id)' cannot be found.", comment: "UTMScriptingConfigImpl")
|
|
|
+ case .invalidDriveDescription: return NSLocalizedString("Drive description is invalid.", comment: "UTMScriptingConfigImpl")
|
|
|
+ case .indexNotFound(let index): return NSLocalizedString("Index \(index) cannot be found.", comment: "UTMScriptingConfigImpl")
|
|
|
+ case .deviceNotSupported: return NSLocalizedString("This device is not supported by the target.", comment: "UTMScriptingConfigImpl")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|