123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- //
- // 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] {
- let wrapper = UTMScriptingConfigImpl(vm.config.wrappedValue as! any UTMConfiguration, data: data)
- return wrapper.serializeConfiguration()
- }
-
- @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
- }
- let wrapper = UTMScriptingConfigImpl(vm.config.wrappedValue as! any UTMConfiguration)
- try wrapper.updateConfiguration(from: newConfiguration)
- try await data.save(vm: vm)
- }
- }
- }
- @MainActor
- class UTMScriptingConfigImpl {
- private var bytesInMib: Int64 {
- 1048576
- }
-
- private(set) var config: any UTMConfiguration
- private weak var data: UTMData?
-
- init(_ config: any UTMConfiguration, data: UTMData? = nil) {
- self.config = config
- self.data = data
- }
-
- func serializeConfiguration() -> [AnyHashable : Any] {
- if let qemuConfig = config as? UTMQemuConfiguration {
- return serializeQemuConfiguration(qemuConfig)
- } else if let appleConfig = config as? UTMAppleConfiguration {
- return serializeAppleConfiguration(appleConfig)
- } else {
- fatalError()
- }
- }
-
- func updateConfiguration(from record: [AnyHashable : Any]) throws {
- if let _ = config as? UTMQemuConfiguration {
- try updateQemuConfiguration(from: record)
- } else if let _ = config as? UTMAppleConfiguration {
- try updateAppleConfiguration(from: record)
- } else {
- fatalError()
- }
- }
-
- private func size(of drive: any UTMConfigurationDrive) -> Int {
- guard let data = data else {
- return 0
- }
- guard let url = drive.imageURL else {
- return 0
- }
- return Int(data.computeSize(for: url) / bytesInMib)
- }
- }
- @MainActor
- extension UTMScriptingConfigImpl {
- 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 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 UTMScriptingConfigImpl {
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMQemuConfiguration
- 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 = config as! UTMAppleConfiguration
- 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 = config as! UTMAppleConfiguration
- 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 = config as! UTMAppleConfiguration
- 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 = config as! UTMAppleConfiguration
- 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 = config as! UTMAppleConfiguration
- 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")
- }
- }
- }
- }
|