Skip to content

Commit 666ad21

Browse files
authored
Merge pull request #528 from insidegui/fix-bridge-list
Simplify network device configuration, fix missing bridges
2 parents ad5c68a + 6de4acf commit 666ad21

File tree

5 files changed

+135
-126
lines changed

5 files changed

+135
-126
lines changed

VirtualBuddy/Config/Versions.xcconfig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
MARKETING_VERSION = 2.0
1+
MARKETING_VERSION = 2.0.1
22
CURRENT_PROJECT_VERSION = 276
33
DYLIB_CURRENT_VERSION = $(CURRENT_PROJECT_VERSION)
44
VERSIONING_SYSTEM = apple-generic

VirtualCore/Source/Definitions/PreviewSupport.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public extension VBMacConfiguration {
5757

5858
c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .managedImage(VBManagedDiskImage(filename: "New Device", size: VBManagedDiskImage.minimumExtraDiskImageSize))))
5959
c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .managedImage(VBManagedDiskImage(filename: "Fake Managed Disk", size: VBManagedDiskImage.minimumExtraDiskImageSize, format: .raw))))
60-
c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .customImage(Bundle.virtualCore.url(forResource: "Fake Custom Path Disk", withExtension: "dmg", subdirectory: "Preview.vbvm")!)))
60+
// c.hardware.storageDevices.append(.init(isBootVolume: false, isEnabled: true, isReadOnly: false, isUSBMassStorageDevice: false, backing: .customImage(Bundle.virtualCore.url(forResource: "Fake Custom Path Disk", withExtension: "dmg", subdirectory: "Preview.vbvm")!)))
6161

6262
c.sharedFolders = [
6363
.init(id: UUID(uuidString: "821BA195-D687-4B61-8412-0C6BA6C99074")!, url: URL(fileURLWithPath: "/Users/insidegui/Desktop"), isReadOnly: true),

VirtualCore/Source/Models/Configuration/ConfigurationModels.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,10 @@ public struct VBDisplayDevice: Identifiable, Hashable, Codable {
215215
/// Configures a network device.
216216
/// **Read the note at the top of this file before modifying this**
217217
public struct VBNetworkDevice: Identifiable, Hashable, Codable {
218-
public init(id: String = "Default", name: String = "Default", kind: VBNetworkDevice.Kind = Kind.NAT, macAddress: String = VZMACAddress.randomLocallyAdministered().string.uppercased()) {
218+
public static let defaultID = "Default"
219+
public static let automaticBridgeID = "Automatic Bridge"
220+
221+
public init(id: String = VBNetworkDevice.defaultID, name: String = "Default", kind: VBNetworkDevice.Kind = Kind.NAT, macAddress: String = VZMACAddress.randomLocallyAdministered().string.uppercased()) {
219222
self.id = id
220223
self.name = name
221224
self.kind = kind
@@ -236,7 +239,7 @@ public struct VBNetworkDevice: Identifiable, Hashable, Codable {
236239
}
237240
}
238241

239-
public var id = "Default"
242+
public var id = VBNetworkDevice.defaultID
240243
public var name = "Default"
241244
public var kind = Kind.NAT
242245
public var macAddress = VZMACAddress.randomLocallyAdministered().string.uppercased()
@@ -558,23 +561,29 @@ public extension VBDisplayPreset {
558561
static var availablePresets: [VBDisplayPreset] { presets.filter(\.isAvailable) }
559562
}
560563

561-
public struct VBNetworkDeviceBridgeInterface: Identifiable {
564+
public struct VBNetworkDeviceInterface: Identifiable, Hashable {
562565
public var id: String
563566
public var name: String
564-
567+
}
568+
569+
extension VBNetworkDeviceInterface {
565570
init(_ interface: VZBridgedNetworkInterface) {
566571
self.id = interface.identifier
567572
self.name = interface.localizedDisplayName ?? interface.identifier
568573
}
569574
}
570575

576+
public extension VBNetworkDeviceInterface {
577+
static let automatic = VBNetworkDeviceInterface(id: VBNetworkDevice.automaticBridgeID, name: "Automatic")
578+
}
579+
571580
public extension VBNetworkDevice {
572581
static var defaultBridgeInterfaceID: String? {
573582
VZBridgedNetworkInterface.networkInterfaces.first?.identifier
574583
}
575584

576-
static var bridgeInterfaces: [VBNetworkDeviceBridgeInterface] {
577-
VZBridgedNetworkInterface.networkInterfaces.map(VBNetworkDeviceBridgeInterface.init)
585+
static var bridgeInterfaces: [VBNetworkDeviceInterface] {
586+
VZBridgedNetworkInterface.networkInterfaces.map(VBNetworkDeviceInterface.init)
578587
}
579588
}
580589

VirtualCore/Source/Virtualization/Helpers/VirtualMachineConfigurationHelper.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Helper that creates various configuration objects exposed in the `VZVirtualMachi
77

88
import Foundation
99
import Virtualization
10+
import BuddyFoundation
1011

1112
protocol VirtualMachineConfigurationHelper {
1213
var vm: VBVirtualMachine { get }
@@ -147,12 +148,13 @@ extension VBNetworkDevice {
147148
}
148149

149150
private func resolveBridge(with identifier: String) throws -> VZBridgedNetworkInterface {
150-
guard let iface = VZBridgedNetworkInterface.networkInterfaces.first(where: { $0.identifier == identifier }) else {
151-
throw Failure("Couldn't find the specified network interface for bridging")
151+
guard identifier != VBNetworkDeviceInterface.automatic.id else {
152+
return try VZBridgedNetworkInterface.networkInterfaces.first.require("There are no network interfaces available on the host for bridging.")
152153
}
153-
return iface
154-
}
155154

155+
return try VZBridgedNetworkInterface.networkInterfaces.first(where: { $0.identifier == identifier })
156+
.require("The bridged network interface \(identifier.quoted) is not available.")
157+
}
156158
}
157159

158160
extension VBPointingDevice {

VirtualUI/Source/VM Configuration/Sections/NetworkConfigurationView.swift

Lines changed: 112 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -7,77 +7,43 @@
77

88
import SwiftUI
99
import VirtualCore
10+
import BuddyFoundation
1011

11-
struct NetworkConfigurationView: View {
12-
13-
@Binding var hardware: VBMacDevice
14-
15-
@State private var previousMACAddress: String?
16-
17-
init(hardware: Binding<VBMacDevice>) {
18-
self._hardware = hardware
19-
self._previousMACAddress = .init(wrappedValue: hardware.wrappedValue.networkDevices.first?.macAddress)
12+
enum NetworkDeviceSelection: Identifiable, Hashable {
13+
var id: String {
14+
switch self {
15+
case .disabled: "DISABLED"
16+
case .NAT: "NAT"
17+
case .bridge(let interfaceID): "BRIDGE_\(interfaceID)"
18+
}
2019
}
2120

22-
private var kind: Binding<VBNetworkDevice.Kind?> {
23-
.init {
24-
hardware.networkDevices.first?.kind
25-
} set: { newValue in
26-
if let newValue {
27-
if hardware.networkDevices.isEmpty {
28-
hardware.networkDevices = [.default]
29-
}
30-
hardware.networkDevices[0].kind = newValue
31-
if let previousMACAddress {
32-
hardware.networkDevices[0].macAddress = previousMACAddress
33-
}
34-
} else {
35-
hardware.networkDevices.removeAll()
36-
}
37-
}
21+
case disabled
22+
case NAT
23+
case bridge(_ interface: VBNetworkDeviceInterface.ID)
24+
}
3825

39-
}
26+
struct NetworkConfigurationView: View {
4027

28+
@Binding var hardware: VBMacDevice
29+
4130
var body: some View {
4231
VStack(alignment: .leading, spacing: 16) {
43-
typePicker
44-
45-
if let kind = kind.wrappedValue {
46-
switch kind {
47-
case .NAT:
48-
natSettings
49-
case .bridge:
50-
bridgeSettings
51-
}
32+
NetworkDevicePicker(hardware: $hardware)
33+
34+
if hardware.networkDevices.isEmpty {
35+
Text("This virtual machine will have no internet or local network access.")
36+
.foregroundColor(.secondary)
5237
} else {
53-
Text("This virtual machine won't have any access to the network.")
54-
.frame(maxWidth: .infinity)
55-
.foregroundColor(.yellow)
56-
.multilineTextAlignment(.center)
57-
}
58-
}
59-
}
60-
61-
@ViewBuilder
62-
private var typePicker: some View {
63-
Picker("Type", selection: kind) {
64-
Text("None")
65-
.tag(Optional<VBNetworkDevice.Kind>.none)
66-
67-
ForEach(VBNetworkDevice.Kind.allCases) { kind in
68-
Text(kind.name)
69-
.tag(Optional<VBNetworkDevice.Kind>.some(kind))
38+
macAddressField
7039
}
7140
}
72-
.pickerStyle(.segmented)
73-
.labelsHidden()
74-
.help("The type of network device")
7541
}
7642

7743
@ViewBuilder
7844
private var macAddressField: some View {
7945
PropertyControl("MAC Address") {
80-
EphemeralTextField($hardware.networkDevices[0].macAddress, alignment: .leading) { addr in
46+
EphemeralTextField($hardware.networkMACAddress, alignment: .leading) { addr in
8147
Text(addr)
8248
.textCase(.uppercase)
8349
} editableContent: { value in
@@ -88,77 +54,109 @@ struct NetworkConfigurationView: View {
8854
return VBNetworkDevice.validateMAC(value)
8955
}
9056
}
91-
.onChange(of: hardware.networkDevices[0].macAddress) { newValue in
92-
previousMACAddress = newValue
57+
}
58+
}
59+
60+
extension VBMacDevice {
61+
var networkDeviceSelection: NetworkDeviceSelection {
62+
get {
63+
if let device = networkDevices.first {
64+
switch device.kind {
65+
case .NAT: .NAT
66+
case .bridge: .bridge(device.id)
67+
}
68+
} else {
69+
.disabled
70+
}
71+
}
72+
set {
73+
let restoreMACAddress = networkMACAddress
74+
75+
switch newValue {
76+
case .disabled:
77+
networkDevices.removeAll()
78+
case .NAT:
79+
networkDevices = [.default.withMACAddress(restoreMACAddress)]
80+
case .bridge(let id):
81+
networkDevices = [.init(id: id, name: id, kind: .bridge).withMACAddress(restoreMACAddress)]
82+
}
83+
}
84+
}
85+
86+
var networkMACAddress: String {
87+
get {
88+
switch networkDeviceSelection {
89+
case .disabled: ""
90+
case .NAT, .bridge: networkDevices.first?.macAddress ?? ""
91+
}
92+
}
93+
set {
94+
switch networkDeviceSelection {
95+
case .disabled: break
96+
case .NAT, .bridge:
97+
guard !networkDevices.isEmpty else { return }
98+
networkDevices[0].macAddress = newValue
99+
}
93100
}
94101
}
95-
96-
@State private var bridgeInterfaces: [VBNetworkDeviceBridgeInterface] = []
97-
98-
@ViewBuilder
99-
private var natSettings: some View {
100-
macAddressField
102+
}
103+
104+
extension VBNetworkDevice {
105+
func withMACAddress(_ address: String) -> VBNetworkDevice {
106+
guard !address.isEmpty else { return self }
107+
var mself = self
108+
mself.macAddress = address
109+
return mself
101110
}
102-
103-
@ViewBuilder
104-
private var bridgeSettings: some View {
105-
if VBNetworkDevice.appSupportsBridgedNetworking {
106-
PropertyControl("Interface") {
107-
HStack {
108-
Picker("Interface", selection: $hardware.networkDevices[0].id) {
109-
if bridgeInterfaces.isEmpty {
110-
Text("No Interfaces Available")
111-
.tag(hardware.networkDevices[0].id)
112-
} else {
113-
ForEach(bridgeInterfaces) { iface in
114-
Text(iface.name)
115-
.tag(iface.id)
116-
}
111+
}
112+
113+
struct NetworkDevicePicker: View {
114+
@Binding var hardware: VBMacDevice
115+
116+
@State private var selectedOption: VBNetworkDeviceInterface?
117+
118+
@State private var interfaces: [VBNetworkDeviceInterface] = [.automatic]
119+
120+
var body: some View {
121+
PropertyControl("Interface") {
122+
HStack {
123+
Picker("Interface", selection: $hardware.networkDeviceSelection) {
124+
Text("Disabled").tag(NetworkDeviceSelection.disabled)
125+
126+
Text("NAT").tag(NetworkDeviceSelection.NAT)
127+
128+
Section("Bridge") {
129+
ForEach(interfaces) { interface in
130+
Text(interface.name)
131+
.tag(NetworkDeviceSelection.bridge(interface.id))
117132
}
118133
}
119-
.disabled(bridgeInterfaces.isEmpty)
120-
121-
Spacer()
122-
123-
Button {
124-
bridgeInterfaces = VBNetworkDevice.bridgeInterfaces
125-
} label: {
126-
Image(systemName: "arrow.clockwise")
127-
}
128-
.buttonStyle(.plain)
129-
.help("Reload interfaces")
130134
}
131-
.onAppear {
132-
bridgeInterfaces = VBNetworkDevice.bridgeInterfaces
135+
.labelsHidden()
136+
137+
Spacer()
138+
139+
Button {
140+
refresh()
141+
} label: {
142+
Image(systemName: "arrow.clockwise")
133143
}
144+
.buttonStyle(.plain)
145+
.help("Reload interfaces")
134146
}
135-
136-
macAddressField
137-
} else {
138-
Text(VBNetworkDevice.bridgeUnsupportedMessage)
139-
.foregroundColor(.red)
147+
.task { refresh() }
140148
}
141149
}
150+
151+
private func refresh() {
152+
interfaces = [.automatic] + VBNetworkDevice.bridgeInterfaces
153+
}
142154
}
143155

144156
#if DEBUG
145-
146-
struct NetworkConfigurationView_Previews: PreviewProvider {
147-
static var previews: some View {
148-
_ConfigurationSectionPreview(.networkPreviewNAT) {
149-
NetworkConfigurationView(hardware: $0.hardware)
150-
}
151-
.previewDisplayName("NAT")
152-
153-
_ConfigurationSectionPreview(.networkPreviewBridge) {
154-
NetworkConfigurationView(hardware: $0.hardware)
155-
}
156-
.previewDisplayName("Bridge")
157-
158-
_ConfigurationSectionPreview(.networkPreviewNone) {
159-
NetworkConfigurationView(hardware: $0.hardware)
160-
}
161-
.previewDisplayName("None")
157+
#Preview {
158+
_ConfigurationSectionPreview(.networkPreviewNAT) {
159+
NetworkConfigurationView(hardware: $0.hardware)
162160
}
163161
}
164162
#endif

0 commit comments

Comments
 (0)