2
0

UTMScriptingUSBDeviceImpl.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. //
  2. // Copyright © 2023 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Foundation
  17. import CocoaSpice
  18. @MainActor
  19. @objc(UTMScriptingUSBDeviceImpl)
  20. class UTMScriptingUSBDeviceImpl: NSObject, UTMScriptable {
  21. @nonobjc var box: CSUSBDevice
  22. private var data: UTMData? {
  23. (NSApp.scriptingDelegate as? AppDelegate)?.data
  24. }
  25. @objc var id: Int {
  26. box.usbBusNumber << 16 | box.usbPortNumber
  27. }
  28. @objc var name: String {
  29. box.name ?? String(format: "%04X:%04X", box.usbVendorId, box.usbProductId)
  30. }
  31. @objc var manufacturerName: String {
  32. box.usbManufacturerName ?? name
  33. }
  34. @objc var productName: String {
  35. box.usbProductName ?? name
  36. }
  37. @objc var vendorId: Int {
  38. box.usbVendorId
  39. }
  40. @objc var productId: Int {
  41. box.usbProductId
  42. }
  43. override var objectSpecifier: NSScriptObjectSpecifier? {
  44. let appDescription = NSApplication.classDescription() as! NSScriptClassDescription
  45. return NSUniqueIDSpecifier(containerClassDescription: appDescription,
  46. containerSpecifier: nil,
  47. key: "scriptingUsbDevices",
  48. uniqueID: id)
  49. }
  50. init(for usbDevice: CSUSBDevice) {
  51. self.box = usbDevice
  52. }
  53. /// Return the same USB device in context of a USB manager
  54. ///
  55. /// This is required because we may be using `CSUSBDevice` objects returned from a different `CSUSBManager`
  56. /// - Parameters:
  57. /// - usbDevice: USB device
  58. /// - usbManager: USB manager
  59. /// - Returns: USB device in same context as the manager
  60. private func same(usbDevice: CSUSBDevice, for usbManager: CSUSBManager) -> CSUSBDevice? {
  61. for other in usbManager.usbDevices {
  62. if other.isEqual(to: usbDevice) {
  63. return other
  64. }
  65. }
  66. return nil
  67. }
  68. @objc func connect(_ command: NSScriptCommand) {
  69. let scriptingVM = command.evaluatedArguments?["vm"] as? UTMScriptingVirtualMachineImpl
  70. withScriptCommand(command) { [self] in
  71. guard let vm = scriptingVM?.vm as? UTMQemuVirtualMachine else {
  72. throw UTMScriptingVirtualMachineImpl.ScriptingError.operationNotSupported
  73. }
  74. guard let usbManager = vm.ioService?.primaryUsbManager else {
  75. throw UTMScriptingVirtualMachineImpl.ScriptingError.operationNotAvailable
  76. }
  77. guard let usbDevice = same(usbDevice: box, for: usbManager) else {
  78. throw ScriptingError.deviceNotFound
  79. }
  80. try await usbManager.connectUsbDevice(usbDevice)
  81. }
  82. }
  83. @objc func disconnect(_ command: NSScriptCommand) {
  84. withScriptCommand(command) { [self] in
  85. guard let data = data else {
  86. throw ScriptingError.notReady
  87. }
  88. let managers = data.virtualMachines.compactMap({ vmdata in
  89. guard let vm = vmdata.wrapped as? UTMQemuVirtualMachine else {
  90. return nil as CSUSBManager?
  91. }
  92. return vm.ioService?.primaryUsbManager
  93. })
  94. guard managers.count > 0 else {
  95. throw UTMScriptingVirtualMachineImpl.ScriptingError.notRunning
  96. }
  97. var found = false
  98. for manager in managers {
  99. if let device = same(usbDevice: box, for: manager), manager.isUsbDeviceConnected(device) {
  100. found = true
  101. try await manager.disconnectUsbDevice(device)
  102. }
  103. }
  104. if !found {
  105. throw ScriptingError.deviceNotConnected
  106. }
  107. }
  108. }
  109. }
  110. // MARK: - Errors
  111. extension UTMScriptingUSBDeviceImpl {
  112. enum ScriptingError: Error, LocalizedError {
  113. case notReady
  114. case deviceNotFound
  115. case deviceNotConnected
  116. var errorDescription: String? {
  117. switch self {
  118. case .notReady: return NSLocalizedString("UTM is not ready to accept commands.", comment: "UTMScriptingUSBDeviceImpl")
  119. case .deviceNotFound: return NSLocalizedString("The device cannot be found.", comment: "UTMScriptingUSBDeviceImpl")
  120. case .deviceNotConnected: return NSLocalizedString("The device is not currently connected.", comment: "UTMScriptingUSBDeviceImpl")
  121. }
  122. }
  123. }
  124. }
  125. // MARK: - NSApplication extension
  126. extension AppDelegate {
  127. @MainActor
  128. @objc var scriptingUsbDevices: [UTMScriptingUSBDeviceImpl] {
  129. guard let data = data else {
  130. return []
  131. }
  132. guard let anyManager = data.virtualMachines.compactMap({ vmData in
  133. guard let vm = vmData.wrapped as? UTMQemuVirtualMachine else {
  134. return nil as CSUSBManager?
  135. }
  136. return vm.ioService?.primaryUsbManager
  137. }).first else {
  138. return []
  139. }
  140. return anyManager.usbDevices.map({ UTMScriptingUSBDeviceImpl(for: $0) })
  141. }
  142. }