Skip to content

Commit f8ce0f9

Browse files
authored
Add an option to disable USB accessories (#1338)
* Add an option to disable USB accessories * Select USB accessories in platform input factories
1 parent 6fff37f commit f8ce0f9

7 files changed

Lines changed: 234 additions & 42 deletions

File tree

‎Sources/tart/Commands/Run.swift‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ struct Run: AsyncParsableCommand {
9090
@Flag(help: "Disable audio pass-through to host.")
9191
var noAudio: Bool = false
9292

93+
@Flag(help: "Disable USB accessories.")
94+
var noUSBAccessories: Bool = false
95+
9396
@Flag(help: ArgumentHelp(
9497
"Disable clipboard sharing between host and guest.",
9598
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 {
470473
nested: nested,
471474
audio: !noAudio,
472475
clipboard: !noClipboard,
476+
noUSBAccessories: noUSBAccessories,
473477
sync: VZDiskImageSynchronizationMode(diskOptions.syncModeRaw),
474478
caching: VZDiskImageCachingMode(diskOptions.cachingModeRaw),
475479
noTrackpad: noTrackpad,

‎Sources/tart/Platform/Darwin.swift‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,40 +104,42 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
104104
return result
105105
}
106106

107-
func keyboards() -> [VZKeyboardConfiguration] {
107+
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration] {
108+
var devices: [VZKeyboardConfiguration] = noUSB ? [] : [VZUSBKeyboardConfiguration()]
108109
if #available(macOS 14, *) {
109110
// Mac keyboard is only supported by guests starting with macOS Ventura
110-
return [VZUSBKeyboardConfiguration(), VZMacKeyboardConfiguration()]
111-
} else {
112-
return [VZUSBKeyboardConfiguration()]
111+
devices.append(VZMacKeyboardConfiguration())
113112
}
113+
return devices
114114
}
115115

116-
func keyboardsSuspendable() -> [VZKeyboardConfiguration] {
116+
func keyboardsSuspendable(noUSB: Bool) -> [VZKeyboardConfiguration] {
117117
if #available(macOS 14, *) {
118118
return [VZMacKeyboardConfiguration()]
119119
} else {
120120
// fallback to the regular configuration
121-
return keyboards()
121+
return keyboards(noUSB: noUSB)
122122
}
123123
}
124124

125-
func pointingDevices() -> [VZPointingDeviceConfiguration] {
125+
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
126126
// Trackpad is only supported by guests starting with macOS Ventura
127-
[VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
127+
var devices: [VZPointingDeviceConfiguration] = noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
128+
devices.append(VZMacTrackpadConfiguration())
129+
return devices
128130
}
129131

130-
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration] {
132+
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
131133
// Only include the USB pointing device, not the trackpad
132-
return [VZUSBScreenCoordinatePointingDeviceConfiguration()]
134+
return noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
133135
}
134136

135-
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
137+
func pointingDevicesSuspendable(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
136138
if #available(macOS 14, *) {
137139
return [VZMacTrackpadConfiguration()]
138140
} else {
139141
// fallback to the regular configuration
140-
return pointingDevices()
142+
return pointingDevices(noUSB: noUSB)
141143
}
142144
}
143145
}

‎Sources/tart/Platform/Linux.swift‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ struct Linux: Platform {
3535
return result
3636
}
3737

38-
func keyboards() -> [VZKeyboardConfiguration] {
39-
[VZUSBKeyboardConfiguration()]
38+
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration] {
39+
noUSB ? [] : [VZUSBKeyboardConfiguration()]
4040
}
4141

42-
func pointingDevices() -> [VZPointingDeviceConfiguration] {
43-
[VZUSBScreenCoordinatePointingDeviceConfiguration()]
42+
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
43+
noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
4444
}
4545

46-
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration] {
46+
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
4747
// Linux doesn't support trackpad, so just return the regular pointing devices
48-
return pointingDevices()
48+
return pointingDevices(noUSB: noUSB)
4949
}
5050
}

‎Sources/tart/Platform/Platform.swift‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ protocol Platform: Codable {
55
func bootLoader(nvramURL: URL) throws -> VZBootLoader
66
func platform(nvramURL: URL, needsNestedVirtualization: Bool) throws -> VZPlatformConfiguration
77
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration
8-
func keyboards() -> [VZKeyboardConfiguration]
9-
func pointingDevices() -> [VZPointingDeviceConfiguration]
10-
func pointingDevicesSimplified() -> [VZPointingDeviceConfiguration]
8+
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration]
9+
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration]
10+
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration]
1111
}
1212

1313
protocol PlatformSuspendable: Platform {
14-
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration]
15-
func keyboardsSuspendable() -> [VZKeyboardConfiguration]
14+
func pointingDevicesSuspendable(noUSB: Bool) -> [VZPointingDeviceConfiguration]
15+
func keyboardsSuspendable(noUSB: Bool) -> [VZKeyboardConfiguration]
1616
}

‎Sources/tart/VM.swift‎

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
4949
nested: Bool = false,
5050
audio: Bool = true,
5151
clipboard: Bool = true,
52+
noUSBAccessories: Bool = false,
5253
sync: VZDiskImageSynchronizationMode = .full,
5354
caching: VZDiskImageCachingMode? = nil,
5455
noTrackpad: Bool = false,
@@ -73,6 +74,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
7374
nested: nested,
7475
audio: audio,
7576
clipboard: clipboard,
77+
noUSBAccessories: noUSBAccessories,
7678
sync: sync,
7779
caching: caching,
7880
noTrackpad: noTrackpad,
@@ -324,6 +326,7 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
324326
nested: Bool = false,
325327
audio: Bool = true,
326328
clipboard: Bool = true,
329+
noUSBAccessories: Bool = false,
327330
sync: VZDiskImageSynchronizationMode = .full,
328331
caching: VZDiskImageCachingMode? = nil,
329332
noTrackpad: Bool = false,
@@ -364,25 +367,15 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
364367
configuration.audioDevices = [soundDeviceConfiguration]
365368

366369
// Keyboard and mouse
367-
if suspendable, let platformSuspendable = vmConfig.platform.self as? PlatformSuspendable {
368-
configuration.keyboards = platformSuspendable.keyboardsSuspendable()
369-
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable()
370-
} else {
371-
372-
if noKeyboard {
373-
configuration.keyboards = []
374-
} else {
375-
configuration.keyboards = vmConfig.platform.keyboards()
376-
}
377-
378-
if noPointer {
379-
configuration.pointingDevices = []
380-
} else if noTrackpad {
381-
configuration.pointingDevices = vmConfig.platform.pointingDevicesSimplified()
382-
} else {
383-
configuration.pointingDevices = vmConfig.platform.pointingDevices()
384-
}
385-
}
370+
configureInputDevices(
371+
configuration,
372+
platform: vmConfig.platform,
373+
suspendable: suspendable,
374+
noUSBAccessories: noUSBAccessories,
375+
noTrackpad: noTrackpad,
376+
noPointer: noPointer,
377+
noKeyboard: noKeyboard
378+
)
386379

387380
// Networking
388381
configuration.networkDevices = network.attachments().map {
@@ -460,6 +453,31 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
460453
return configuration
461454
}
462455

456+
static func configureInputDevices(
457+
_ configuration: VZVirtualMachineConfiguration,
458+
platform: Platform,
459+
suspendable: Bool = false,
460+
noUSBAccessories: Bool = false,
461+
noTrackpad: Bool = false,
462+
noPointer: Bool = false,
463+
noKeyboard: Bool = false
464+
) {
465+
if suspendable, let platformSuspendable = platform as? PlatformSuspendable {
466+
configuration.keyboards = platformSuspendable.keyboardsSuspendable(noUSB: noUSBAccessories)
467+
configuration.pointingDevices = platformSuspendable.pointingDevicesSuspendable(noUSB: noUSBAccessories)
468+
} else {
469+
configuration.keyboards = noKeyboard ? [] : platform.keyboards(noUSB: noUSBAccessories)
470+
471+
if noPointer {
472+
configuration.pointingDevices = []
473+
} else if noTrackpad {
474+
configuration.pointingDevices = platform.pointingDevicesSimplified(noUSB: noUSBAccessories)
475+
} else {
476+
configuration.pointingDevices = platform.pointingDevices(noUSB: noUSBAccessories)
477+
}
478+
}
479+
}
480+
463481
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
464482
print("guest has stopped the virtual machine")
465483
sema.signal()

‎Tests/TartTests/CommandBehaviorTests.swift‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ import XCTest
44
@testable import tart
55

66
final class CommandBehaviorTests: XCTestCase {
7+
func testNoUSBAccessoriesDoesNotEnableSuspendable() throws {
8+
try withTemporaryTartHome {
9+
let vmDir = try VMStorageLocal().create("no-usb-accessories")
10+
try config().save(toURL: vmDir.configURL)
11+
XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.nvramURL.path, contents: Data()))
12+
XCTAssertTrue(FileManager.default.createFile(atPath: vmDir.diskURL.path, contents: Data()))
13+
14+
let command = try Run.parseAsRoot(["no-usb-accessories", "--no-usb-accessories"]) as! Run
15+
16+
XCTAssertTrue(command.noUSBAccessories)
17+
XCTAssertFalse(command.suspendable)
18+
XCTAssertFalse(command.noAudio)
19+
XCTAssertFalse(command.noGraphics)
20+
}
21+
}
22+
723
func testStandaloneDeleteDoesNotInitializeContentStore() throws {
824
try withTemporaryTartHome {
925
let vmDir = try VMStorageLocal().create("standalone")
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import Virtualization
2+
import XCTest
3+
@testable import tart
4+
5+
final class InputDeviceConfigurationTests: XCTestCase {
6+
func testLinuxUSBInputsCanBeDisabled() {
7+
let configuration = VZVirtualMachineConfiguration()
8+
9+
VM.configureInputDevices(configuration, platform: Linux())
10+
XCTAssertEqual(configuration.keyboards.count, 1)
11+
XCTAssertTrue(configuration.keyboards.contains { $0 is VZUSBKeyboardConfiguration })
12+
XCTAssertEqual(configuration.pointingDevices.count, 1)
13+
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZUSBScreenCoordinatePointingDeviceConfiguration })
14+
15+
VM.configureInputDevices(configuration, platform: Linux(), noUSBAccessories: true)
16+
XCTAssertTrue(configuration.keyboards.isEmpty)
17+
XCTAssertTrue(configuration.pointingDevices.isEmpty)
18+
19+
VM.configureInputDevices(configuration, platform: Linux(), noUSBAccessories: true, noTrackpad: true)
20+
XCTAssertTrue(configuration.keyboards.isEmpty)
21+
XCTAssertTrue(configuration.pointingDevices.isEmpty)
22+
}
23+
24+
#if arch(arm64)
25+
func testMacOS13RetainsItsNativeTrackpad() {
26+
let platform = MacInputPlatform(nativeKeyboard: false)
27+
let configuration = VZVirtualMachineConfiguration()
28+
29+
VM.configureInputDevices(configuration, platform: platform)
30+
XCTAssertEqual(configuration.keyboards.count, 1)
31+
XCTAssertEqual(configuration.pointingDevices.count, 2)
32+
33+
VM.configureInputDevices(configuration, platform: platform, noUSBAccessories: true)
34+
XCTAssertTrue(configuration.keyboards.isEmpty)
35+
XCTAssertEqual(configuration.pointingDevices.count, 1)
36+
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration })
37+
}
38+
39+
func testMacOS14RetainsBothNativeInputs() throws {
40+
guard #available(macOS 14, *) else {
41+
throw XCTSkip("Mac keyboards require macOS 14")
42+
}
43+
44+
let configuration = VZVirtualMachineConfiguration()
45+
VM.configureInputDevices(configuration, platform: MacInputPlatform(nativeKeyboard: true), noUSBAccessories: true)
46+
47+
XCTAssertEqual(configuration.keyboards.count, 1)
48+
XCTAssertTrue(configuration.keyboards.contains { $0 is VZMacKeyboardConfiguration })
49+
XCTAssertEqual(configuration.pointingDevices.count, 1)
50+
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration })
51+
}
52+
53+
func testInputFlagsStillSelectTheExpectedDevices() throws {
54+
guard #available(macOS 14, *) else {
55+
throw XCTSkip("Mac keyboards require macOS 14")
56+
}
57+
58+
let platform = MacInputPlatform(nativeKeyboard: true)
59+
for noUSBAccessories in [false, true] {
60+
for noKeyboard in [false, true] {
61+
for noPointer in [false, true] {
62+
for noTrackpad in [false, true] {
63+
let configuration = VZVirtualMachineConfiguration()
64+
VM.configureInputDevices(
65+
configuration,
66+
platform: platform,
67+
noUSBAccessories: noUSBAccessories,
68+
noTrackpad: noTrackpad,
69+
noPointer: noPointer,
70+
noKeyboard: noKeyboard
71+
)
72+
73+
XCTAssertEqual(configuration.keyboards.contains { $0 is VZUSBKeyboardConfiguration }, !noUSBAccessories && !noKeyboard)
74+
XCTAssertEqual(configuration.keyboards.contains { $0 is VZMacKeyboardConfiguration }, !noKeyboard)
75+
XCTAssertEqual(configuration.pointingDevices.contains { $0 is VZUSBScreenCoordinatePointingDeviceConfiguration }, !noUSBAccessories && !noPointer)
76+
XCTAssertEqual(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration }, !noPointer && !noTrackpad)
77+
}
78+
}
79+
}
80+
}
81+
}
82+
83+
func testSuspendableFallbackCannotReintroduceUSBInputs() {
84+
let configuration = VZVirtualMachineConfiguration()
85+
let platform = MacInputPlatform(nativeKeyboard: false)
86+
VM.configureInputDevices(configuration, platform: platform, suspendable: true)
87+
XCTAssertEqual(configuration.keyboards.count, 1)
88+
XCTAssertEqual(configuration.pointingDevices.count, 2)
89+
90+
VM.configureInputDevices(
91+
configuration,
92+
platform: platform,
93+
suspendable: true,
94+
noUSBAccessories: true
95+
)
96+
97+
XCTAssertTrue(configuration.keyboards.isEmpty)
98+
XCTAssertEqual(configuration.pointingDevices.count, 1)
99+
XCTAssertTrue(configuration.pointingDevices.contains { $0 is VZMacTrackpadConfiguration })
100+
}
101+
#endif
102+
}
103+
104+
#if arch(arm64)
105+
// Model macOS 13 and 14 input availability without requiring a second host.
106+
private struct MacInputPlatform: PlatformSuspendable {
107+
var nativeKeyboard: Bool
108+
109+
func os() -> OS { .darwin }
110+
111+
func bootLoader(nvramURL: URL) throws -> VZBootLoader {
112+
try Linux().bootLoader(nvramURL: nvramURL)
113+
}
114+
115+
func platform(nvramURL: URL, needsNestedVirtualization: Bool) throws -> VZPlatformConfiguration {
116+
try Linux().platform(nvramURL: nvramURL, needsNestedVirtualization: needsNestedVirtualization)
117+
}
118+
119+
func graphicsDevice(vmConfig: VMConfig) -> VZGraphicsDeviceConfiguration {
120+
Linux().graphicsDevice(vmConfig: vmConfig)
121+
}
122+
123+
func keyboards(noUSB: Bool) -> [VZKeyboardConfiguration] {
124+
var devices: [VZKeyboardConfiguration] = noUSB ? [] : [VZUSBKeyboardConfiguration()]
125+
if nativeKeyboard, #available(macOS 14, *) {
126+
devices.append(VZMacKeyboardConfiguration())
127+
}
128+
return devices
129+
}
130+
131+
func pointingDevices(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
132+
var devices: [VZPointingDeviceConfiguration] = noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
133+
devices.append(VZMacTrackpadConfiguration())
134+
return devices
135+
}
136+
137+
func pointingDevicesSimplified(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
138+
noUSB ? [] : [VZUSBScreenCoordinatePointingDeviceConfiguration()]
139+
}
140+
141+
func keyboardsSuspendable(noUSB: Bool) -> [VZKeyboardConfiguration] {
142+
if nativeKeyboard, #available(macOS 14, *) {
143+
return [VZMacKeyboardConfiguration()]
144+
}
145+
return keyboards(noUSB: noUSB)
146+
}
147+
148+
func pointingDevicesSuspendable(noUSB: Bool) -> [VZPointingDeviceConfiguration] {
149+
nativeKeyboard ? [VZMacTrackpadConfiguration()] : pointingDevices(noUSB: noUSB)
150+
}
151+
}
152+
#endif

0 commit comments

Comments
 (0)