UTM/Scripting/UTMScriptingUSBDeviceImpl.s...

160 lines
5.5 KiB
Swift

//
// 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
import CocoaSpice
@MainActor
@objc(UTMScriptingUSBDeviceImpl)
class UTMScriptingUSBDeviceImpl: NSObject, UTMScriptable {
@nonobjc var box: CSUSBDevice
private var data: UTMData? {
(NSApp.scriptingDelegate as? AppDelegate)?.data
}
@objc var id: Int {
box.usbBusNumber << 16 | box.usbPortNumber
}
@objc var name: String {
box.name ?? String(format: "%04X:%04X", box.usbVendorId, box.usbProductId)
}
@objc var manufacturerName: String {
box.usbManufacturerName ?? name
}
@objc var productName: String {
box.usbProductName ?? name
}
@objc var vendorId: Int {
box.usbVendorId
}
@objc var productId: Int {
box.usbProductId
}
override var objectSpecifier: NSScriptObjectSpecifier? {
let appDescription = NSApplication.classDescription() as! NSScriptClassDescription
return NSUniqueIDSpecifier(containerClassDescription: appDescription,
containerSpecifier: nil,
key: "scriptingUsbDevices",
uniqueID: id)
}
init(for usbDevice: CSUSBDevice) {
self.box = usbDevice
}
/// Return the same USB device in context of a USB manager
///
/// This is required because we may be using `CSUSBDevice` objects returned from a different `CSUSBManager`
/// - Parameters:
/// - usbDevice: USB device
/// - usbManager: USB manager
/// - Returns: USB device in same context as the manager
private func same(usbDevice: CSUSBDevice, for usbManager: CSUSBManager) -> CSUSBDevice? {
for other in usbManager.usbDevices {
if other.isEqual(to: usbDevice) {
return other
}
}
return nil
}
@objc func connect(_ command: NSScriptCommand) {
let scriptingVM = command.evaluatedArguments?["vm"] as? UTMScriptingVirtualMachineImpl
withScriptCommand(command) { [self] in
guard let vm = scriptingVM?.vm as? UTMQemuVirtualMachine else {
throw UTMScriptingVirtualMachineImpl.ScriptingError.operationNotSupported
}
guard let usbManager = vm.ioService?.primaryUsbManager else {
throw UTMScriptingVirtualMachineImpl.ScriptingError.operationNotAvailable
}
guard let usbDevice = same(usbDevice: box, for: usbManager) else {
throw ScriptingError.deviceNotFound
}
try await usbManager.connectUsbDevice(usbDevice)
}
}
@objc func disconnect(_ command: NSScriptCommand) {
withScriptCommand(command) { [self] in
guard let data = data else {
throw ScriptingError.notReady
}
let managers = data.virtualMachines.compactMap({ vmdata in
guard let vm = vmdata.wrapped as? UTMQemuVirtualMachine else {
return nil as CSUSBManager?
}
return vm.ioService?.primaryUsbManager
})
guard managers.count > 0 else {
throw UTMScriptingVirtualMachineImpl.ScriptingError.notRunning
}
var found = false
for manager in managers {
if let device = same(usbDevice: box, for: manager), manager.isUsbDeviceConnected(device) {
found = true
try await manager.disconnectUsbDevice(device)
}
}
if !found {
throw ScriptingError.deviceNotConnected
}
}
}
}
// MARK: - Errors
extension UTMScriptingUSBDeviceImpl {
enum ScriptingError: Error, LocalizedError {
case notReady
case deviceNotFound
case deviceNotConnected
var errorDescription: String? {
switch self {
case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingUSBDeviceImpl")
case .deviceNotFound: return NSLocalizedString("The device cannot be found.", comment: "UTMScriptingUSBDeviceImpl")
case .deviceNotConnected: return NSLocalizedString("The device is not currently connected.", comment: "UTMScriptingUSBDeviceImpl")
}
}
}
}
// MARK: - NSApplication extension
extension AppDelegate {
@MainActor
@objc var scriptingUsbDevices: [UTMScriptingUSBDeviceImpl] {
guard let data = data else {
return []
}
guard let anyManager = data.virtualMachines.compactMap({ vmData in
guard let vm = vmData.wrapped as? UTMQemuVirtualMachine else {
return nil as CSUSBManager?
}
return vm.ioService?.primaryUsbManager
}).first else {
return []
}
return anyManager.usbDevices.map({ UTMScriptingUSBDeviceImpl(for: $0) })
}
}