Add an option to disable USB accessories (#1338)

* Add an option to disable USB accessories

* Select USB accessories in platform input factories
This commit is contained in:
Yibo Zhuang
2026-09-23 12:14:39 -07:00
committed by GitHub
parent 6fff37f0e0
commit f8ce0f9acb
7 changed files with 234 additions and 42 deletions
+4
View File
@@ -90,6 +90,9 @@ struct Run: AsyncParsableCommand {
@Flag(help: "Disable audio pass-through to host.")
var noAudio: Bool = false
@Flag(help: "Disable USB accessories.")
var noUSBAccessories: Bool = false
@Flag(help: ArgumentHelp(
"Disable clipboard sharing between host and guest.",
discussion: "Clipboard sharing requires spice-vdagent package on Linux and https://github.com/cirruslabs/tart-guest-agent on macOS."))
@@ -470,6 +473,7 @@ struct Run: AsyncParsableCommand {
nested: nested,
audio: !noAudio,
clipboard: !noClipboard,
noUSBAccessories: noUSBAccessories,
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw),
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw),
noTrackpad: noTrackpad,
+14 -12
View File
@@ -104,40 +104,42 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
return result
}
func keyboards() -> [VZKeyboardConfiguration] {
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration] {
var devices: [VZKeyboardConfiguration] = noUSB ? [] : [VZUSBKeyboardConfiguration()]
if #available(macOS 14, *) {
// Mac keyboard is only supported by guests starting with macOS Ventura
return [VZUSBKeyboardConfiguration(), VZMacKeyboardConfiguration()]
} else {
return [VZUSBKeyboardConfiguration()]
devices.append(VZMacKeyboardConfiguration())
}
return devices
}
func keyboardsSuspendable() -> [VZKeyboardConfiguration] {
func keyboardsSuspendable(noUSB: Bool) -> [VZKeyboardConfiguration] {
if #available(macOS 14, *) {
return [VZMacKeyboardConfiguration()]
} else {
// fallback to the regular configuration
return keyboards()
return keyboards(noUSB: noUSB)
}
}
func pointingDevices() -> [VZPointingDeviceConfiguration] {
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
// Trackpad is only supported by guests starting with macOS Ventura
[VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
var devices: [VZPointingDeviceConfiguration] = noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
devices.append(VZMacTrackpadConfiguration())
return devices
}
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration] {
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
// Only include the USB pointing device, not the trackpad
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
return noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
}
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
func pointingDevicesSuspendable(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
if #available(macOS 14, *) {
return [VZMacTrackpadConfiguration()]
} else {
// fallback to the regular configuration
return pointingDevices()
return pointingDevices(noUSB: noUSB)
}
}
}
+6 -6
View File
@@ -35,16 +35,16 @@ struct Linux: Platform {
return result
}
func keyboards() -> [VZKeyboardConfiguration] {
[VZUSBKeyboardConfiguration()]
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration] {
noUSB ? [] : [VZUSBKeyboardConfiguration()]
}
func pointingDevices() -> [VZPointingDeviceConfiguration] {
[VZUSBScreenCoordinatePointingDeviceConfiguration()]
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
}
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration] {
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
// Linux doesn't support trackpad, so just return the regular pointing devices
return pointingDevices()
return pointingDevices(noUSB: noUSB)
}
}
+5 -5
View File
@@ -5,12 +5,12 @@ protocol Platform: Codable {
func bootLoader(nvramURL: URL) throws -> VZBootLoader
func platform(nvramURL: URL, needsNestedVirtualization: Bool) throws -> VZPlatformConfiguration
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
func keyboards() -> [VZKeyboardConfiguration]
func pointingDevices() -> [VZPointingDeviceConfiguration]
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration]
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration]
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration]
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration]
}
protocol PlatformSuspendable: Platform {
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration]
func keyboardsSuspendable() -> [VZKeyboardConfiguration]
func pointingDevicesSuspendable(noUSB: Bool) -> [VZPointingDeviceConfiguration]
func keyboardsSuspendable(noUSB: Bool) -> [VZKeyboardConfiguration]
}
+37 -19
View File
@@ -49,6 +49,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
nested: Bool = false,
audio: Bool = true,
clipboard: Bool = true,
noUSBAccessories: Bool = false,
sync: VZDiskImageSynchronizationMode = .full,
caching: VZDiskImageCachingMode? = nil,
noTrackpad: Bool = false,
@@ -73,6 +74,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
nested: nested,
audio: audio,
clipboard: clipboard,
noUSBAccessories: noUSBAccessories,
sync: sync,
caching: caching,
noTrackpad: noTrackpad,
@@ -324,6 +326,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
nested: Bool = false,
audio: Bool = true,
clipboard: Bool = true,
noUSBAccessories: Bool = false,
sync: VZDiskImageSynchronizationMode = .full,
caching: VZDiskImageCachingMode? = nil,
noTrackpad: Bool = false,
@@ -364,25 +367,15 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
configuration.audioDevices = [soundDeviceConfiguration]
// Keyboard and mouse
if suspendable, let platformSuspendable = vmConfig.platform.self as? PlatformSuspendable {
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
} else {
if noKeyboard {
configuration.keyboards = []
} else {
configuration.keyboards = vmConfig.platform.keyboards()
}
if noPointer {
configuration.pointingDevices = []
} else if noTrackpad {
configuration.pointingDevices = vmConfig.platform.pointingDevicesSimplified()
} else {
configuration.pointingDevices = vmConfig.platform.pointingDevices()
}
}
configureInputDevices(
configuration,
platform: vmConfig.platform,
suspendable: suspendable,
noUSBAccessories: noUSBAccessories,
noTrackpad: noTrackpad,
noPointer: noPointer,
noKeyboard: noKeyboard
)
// Networking
configuration.networkDevices = network.attachments().map {
@@ -460,6 +453,31 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
return configuration
}
static func configureInputDevices(
_ configuration: VZVirtualMachineConfiguration,
platform: Platform,
suspendable: Bool = false,
noUSBAccessories: Bool = false,
noTrackpad: Bool = false,
noPointer: Bool = false,
noKeyboard: Bool = false
) {
if suspendable, let platformSuspendable = platform as? PlatformSuspendable {
configuration.keyboards = platformSuspendable.keyboardsSuspendable(noUSB: noUSBAccessories)
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable(noUSB: noUSBAccessories)
} else {
configuration.keyboards = noKeyboard ? [] : platform.keyboards(noUSB: noUSBAccessories)
if noPointer {
configuration.pointingDevices = []
} else if noTrackpad {
configuration.pointingDevices = platform.pointingDevicesSimplified(noUSB: noUSBAccessories)
} else {
configuration.pointingDevices = platform.pointingDevices(noUSB: noUSBAccessories)
}
}
}
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
print("guest has stopped the virtual machine")
sema.signal()
@@ -4,6 +4,22 @@ import XCTest
@testable import tart
final class CommandBehaviorTests: XCTestCase {
func testNoUSBAccessoriesDoesNotEnableSuspendable() throws {
try withTemporaryTartHome {
let vmDir = try VMStorageLocal().create("no-usb-accessories")
try config().save(toURL: vmDir.configURL)
XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.nvramURL.path, contents: Data()))
XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.diskURL.path, contents: Data()))
let command = try Run.parseAsRoot(["no-usb-accessories", "--no-usb-accessories"]) as! Run
XCTAssertTrue(command.noUSBAccessories)
XCTAssertFalse(command.suspendable)
XCTAssertFalse(command.noAudio)
XCTAssertFalse(command.noGraphics)
}
}
func testStandaloneDeleteDoesNotInitializeContentStore() throws {
try withTemporaryTartHome {
let vmDir = try VMStorageLocal().create("standalone")
@@ -0,0 +1,152 @@
import Virtualization
import XCTest
@testable import tart
final class InputDeviceConfigurationTests: XCTestCase {
func testLinuxUSBInputsCanBeDisabled() {
let configuration = VZVirtualMachineConfiguration()
VM.configureInputDevices(configuration, platform: Linux())
XCTAssertEqual(configuration.keyboards.count, 1)
XCTAssertTrue(configuration.keyboards.contains { $0 is VZUSBKeyboardConfiguration })
XCTAssertEqual(configuration.pointingDevices.count, 1)
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZUSBScreenCoordinatePointingDeviceConfiguration })
VM.configureInputDevices(configuration, platform: Linux(), noUSBAccessories: true)
XCTAssertTrue(configuration.keyboards.isEmpty)
XCTAssertTrue(configuration.pointingDevices.isEmpty)
VM.configureInputDevices(configuration, platform: Linux(), noUSBAccessories: true, noTrackpad: true)
XCTAssertTrue(configuration.keyboards.isEmpty)
XCTAssertTrue(configuration.pointingDevices.isEmpty)
}
#if arch(arm64)
func testMacOS13RetainsItsNativeTrackpad() {
let platform = MacInputPlatform(nativeKeyboard: false)
let configuration = VZVirtualMachineConfiguration()
VM.configureInputDevices(configuration, platform: platform)
XCTAssertEqual(configuration.keyboards.count, 1)
XCTAssertEqual(configuration.pointingDevices.count, 2)
VM.configureInputDevices(configuration, platform: platform, noUSBAccessories: true)
XCTAssertTrue(configuration.keyboards.isEmpty)
XCTAssertEqual(configuration.pointingDevices.count, 1)
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration })
}
func testMacOS14RetainsBothNativeInputs() throws {
guard #available(macOS 14, *) else {
throw XCTSkip("Mac keyboards require macOS 14")
}
let configuration = VZVirtualMachineConfiguration()
VM.configureInputDevices(configuration, platform: MacInputPlatform(nativeKeyboard: true), noUSBAccessories: true)
XCTAssertEqual(configuration.keyboards.count, 1)
XCTAssertTrue(configuration.keyboards.contains { $0 is VZMacKeyboardConfiguration })
XCTAssertEqual(configuration.pointingDevices.count, 1)
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration })
}
func testInputFlagsStillSelectTheExpectedDevices() throws {
guard #available(macOS 14, *) else {
throw XCTSkip("Mac keyboards require macOS 14")
}
let platform = MacInputPlatform(nativeKeyboard: true)
for noUSBAccessories in [false, true] {
for noKeyboard in [false, true] {
for noPointer in [false, true] {
for noTrackpad in [false, true] {
let configuration = VZVirtualMachineConfiguration()
VM.configureInputDevices(
configuration,
platform: platform,
noUSBAccessories: noUSBAccessories,
noTrackpad: noTrackpad,
noPointer: noPointer,
noKeyboard: noKeyboard
)
XCTAssertEqual(configuration.keyboards.contains { $0 is VZUSBKeyboardConfiguration }, !noUSBAccessories && !noKeyboard)
XCTAssertEqual(configuration.keyboards.contains { $0 is VZMacKeyboardConfiguration }, !noKeyboard)
XCTAssertEqual(configuration.pointingDevices.contains { $0 is VZUSBScreenCoordinatePointingDeviceConfiguration }, !noUSBAccessories && !noPointer)
XCTAssertEqual(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration }, !noPointer && !noTrackpad)
}
}
}
}
}
func testSuspendableFallbackCannotReintroduceUSBInputs() {
let configuration = VZVirtualMachineConfiguration()
let platform = MacInputPlatform(nativeKeyboard: false)
VM.configureInputDevices(configuration, platform: platform, suspendable: true)
XCTAssertEqual(configuration.keyboards.count, 1)
XCTAssertEqual(configuration.pointingDevices.count, 2)
VM.configureInputDevices(
configuration,
platform: platform,
suspendable: true,
noUSBAccessories: true
)
XCTAssertTrue(configuration.keyboards.isEmpty)
XCTAssertEqual(configuration.pointingDevices.count, 1)
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration })
}
#endif
}
#if arch(arm64)
// Model macOS 13 and 14 input availability without requiring a second host.
private struct MacInputPlatform: PlatformSuspendable {
var nativeKeyboard: Bool
func os() -> OS { .darwin }
func bootLoader(nvramURL: URL) throws -> VZBootLoader {
try Linux().bootLoader(nvramURL: nvramURL)
}
func platform(nvramURL: URL, needsNestedVirtualization: Bool) throws -> VZPlatformConfiguration {
try Linux().platform(nvramURL: nvramURL, needsNestedVirtualization: needsNestedVirtualization)
}
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
Linux().graphicsDevice(vmConfig: vmConfig)
}
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration] {
var devices: [VZKeyboardConfiguration] = noUSB ? [] : [VZUSBKeyboardConfiguration()]
if nativeKeyboard, #available(macOS 14, *) {
devices.append(VZMacKeyboardConfiguration())
}
return devices
}
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
var devices: [VZPointingDeviceConfiguration] = noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
devices.append(VZMacTrackpadConfiguration())
return devices
}
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
}
func keyboardsSuspendable(noUSB: Bool) -> [VZKeyboardConfiguration] {
if nativeKeyboard, #available(macOS 14, *) {
return [VZMacKeyboardConfiguration()]
}
return keyboards(noUSB: noUSB)
}
func pointingDevicesSuspendable(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
nativeKeyboard ? [VZMacTrackpadConfiguration()] : pointingDevices(noUSB: noUSB)
}
}
#endif