diff --git a/configuration.go b/configuration.go index 4a7ea3f7..d18c03e1 100644 --- a/configuration.go +++ b/configuration.go @@ -38,6 +38,7 @@ type VirtualMachineConfiguration struct { memorySize uint64 *pointer + networkDeviceConfiguration []*VirtioNetworkDeviceConfiguration storageDeviceConfiguration []StorageDeviceConfiguration } @@ -116,20 +117,13 @@ func (v *VirtualMachineConfiguration) SetNetworkDevicesVirtualMachineConfigurati } array := objc.ConvertToNSMutableArray(ptrs) C.setNetworkDevicesVZVirtualMachineConfiguration(objc.Ptr(v), objc.Ptr(array)) + v.networkDeviceConfiguration = cs } // NetworkDevices return the list of network device configuration set in this virtual machine configuration. // Return an empty array if no network device configuration is set. func (v *VirtualMachineConfiguration) NetworkDevices() []*VirtioNetworkDeviceConfiguration { - nsArray := objc.NewNSArray( - C.networkDevicesVZVirtualMachineConfiguration(objc.Ptr(v)), - ) - ptrs := nsArray.ToPointerSlice() - networkDevices := make([]*VirtioNetworkDeviceConfiguration, len(ptrs)) - for i, ptr := range ptrs { - networkDevices[i] = newVirtioNetworkDeviceConfiguration(ptr) - } - return networkDevices + return v.networkDeviceConfiguration } // SetSerialPortsVirtualMachineConfiguration sets list of serial ports. Empty by default. diff --git a/internal/sliceutil/sliceutil.go b/internal/sliceutil/sliceutil.go new file mode 100644 index 00000000..05c37d4f --- /dev/null +++ b/internal/sliceutil/sliceutil.go @@ -0,0 +1,10 @@ +package sliceutil + +// FindValueByIndex returns the value of the index in s, +// or zero value if not present. +func FindValueByIndex[S ~[]E, E any](s S, idx int) (v E) { + if idx < 0 || idx >= len(s) { + return v // return zero value of type E + } + return s[idx] +} diff --git a/internal/sliceutil/sliceutil_test.go b/internal/sliceutil/sliceutil_test.go new file mode 100644 index 00000000..c4350974 --- /dev/null +++ b/internal/sliceutil/sliceutil_test.go @@ -0,0 +1,50 @@ +package sliceutil_test + +import ( + "testing" + + "github.com/Code-Hex/vz/v3/internal/sliceutil" +) + +func TestFindValueByIndex(t *testing.T) { + tests := []struct { + name string + slice []int + index int + expected int + }{ + { + name: "Index within range", + slice: []int{1, 2, 3, 4, 5}, + index: 2, + expected: 3, + }, + { + name: "Index out of range", + slice: []int{1, 2, 3, 4, 5}, + index: 10, + expected: 0, // default value of int + }, + { + name: "Negative index", + slice: []int{1, 2, 3, 4, 5}, + index: -1, + expected: 0, // default value of int + }, + { + name: "Empty slice", + slice: []int{}, + index: 0, + expected: 0, // default value of int + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := sliceutil.FindValueByIndex(tt.slice, tt.index) + if result != tt.expected { + t.Errorf("FindValueByIndex(%v, %d) = %v; want %v", tt.slice, tt.index, result, tt.expected) + } + }) + } +} diff --git a/network.go b/network.go index 0250e6ce..c3ecddc8 100644 --- a/network.go +++ b/network.go @@ -12,7 +12,6 @@ import ( "net" "os" "syscall" - "unsafe" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -91,6 +90,10 @@ type NATNetworkDeviceAttachment struct { *baseNetworkDeviceAttachment } +func (*NATNetworkDeviceAttachment) String() string { + return "NATNetworkDeviceAttachment" +} + var _ NetworkDeviceAttachment = (*NATNetworkDeviceAttachment)(nil) // NewNATNetworkDeviceAttachment creates a new NATNetworkDeviceAttachment. @@ -127,6 +130,10 @@ type BridgedNetworkDeviceAttachment struct { *baseNetworkDeviceAttachment } +func (*BridgedNetworkDeviceAttachment) String() string { + return "BridgedNetworkDeviceAttachment" +} + var _ NetworkDeviceAttachment = (*BridgedNetworkDeviceAttachment)(nil) // NewBridgedNetworkDeviceAttachment creates a new BridgedNetworkDeviceAttachment with networkInterface. @@ -164,6 +171,10 @@ type FileHandleNetworkDeviceAttachment struct { mtu int } +func (*FileHandleNetworkDeviceAttachment) String() string { + return "FileHandleNetworkDeviceAttachment" +} + var _ NetworkDeviceAttachment = (*FileHandleNetworkDeviceAttachment)(nil) // NewFileHandleNetworkDeviceAttachment initialize the attachment with a file handle. @@ -253,7 +264,7 @@ func (f *FileHandleNetworkDeviceAttachment) MaximumTransmissionUnit() int { // see: https://developer.apple.com/documentation/virtualization/vznetworkdeviceattachment?language=objc type NetworkDeviceAttachment interface { objc.NSObject - + fmt.Stringer networkDeviceAttachment() } @@ -271,6 +282,8 @@ func (*baseNetworkDeviceAttachment) networkDeviceAttachment() {} // see: https://developer.apple.com/documentation/virtualization/vzvirtionetworkdeviceconfiguration?language=objc type VirtioNetworkDeviceConfiguration struct { *pointer + + attachment NetworkDeviceAttachment } // NewVirtioNetworkDeviceConfiguration creates a new VirtioNetworkDeviceConfiguration with NetworkDeviceAttachment. @@ -282,20 +295,20 @@ func NewVirtioNetworkDeviceConfiguration(attachment NetworkDeviceAttachment) (*V return nil, err } - config := newVirtioNetworkDeviceConfiguration( - C.newVZVirtioNetworkDeviceConfiguration( - objc.Ptr(attachment), - ), - ) + config := newVirtioNetworkDeviceConfiguration(attachment) objc.SetFinalizer(config, func(self *VirtioNetworkDeviceConfiguration) { objc.Release(self) }) return config, nil } -func newVirtioNetworkDeviceConfiguration(ptr unsafe.Pointer) *VirtioNetworkDeviceConfiguration { +func newVirtioNetworkDeviceConfiguration(attachment NetworkDeviceAttachment) *VirtioNetworkDeviceConfiguration { + ptr := C.newVZVirtioNetworkDeviceConfiguration( + objc.Ptr(attachment), + ) return &VirtioNetworkDeviceConfiguration{ - pointer: objc.NewPointer(ptr), + pointer: objc.NewPointer(ptr), + attachment: attachment, } } @@ -303,6 +316,10 @@ func (v *VirtioNetworkDeviceConfiguration) SetMACAddress(macAddress *MACAddress) C.setNetworkDevicesVZMACAddress(objc.Ptr(v), objc.Ptr(macAddress)) } +func (v *VirtioNetworkDeviceConfiguration) Attachment() NetworkDeviceAttachment { + return v.attachment +} + // MACAddress represents a media access control address (MAC address), the 48-bit ethernet address. // see: https://developer.apple.com/documentation/virtualization/vzmacaddress?language=objc type MACAddress struct { diff --git a/virtualization.go b/virtualization.go index 8d0a2c91..1ca3ba79 100644 --- a/virtualization.go +++ b/virtualization.go @@ -9,12 +9,14 @@ package vz */ import "C" import ( + "fmt" "runtime/cgo" "sync" "unsafe" infinity "github.com/Code-Hex/go-infinity-channel" "github.com/Code-Hex/vz/v3/internal/objc" + "github.com/Code-Hex/vz/v3/internal/sliceutil" ) // VirtualMachineState represents execution state of the virtual machine. @@ -91,9 +93,15 @@ type VirtualMachine struct { dispatchQueue unsafe.Pointer machineState *machineState + disconnectedIn *infinity.Channel[*disconnected] + disconnectedOut *infinity.Channel[*DisconnectedError] + watchDisconnectedOnce sync.Once + finalizeOnce sync.Once config *VirtualMachineConfiguration + + mu sync.RWMutex } type machineState struct { @@ -123,8 +131,12 @@ func NewVirtualMachine(config *VirtualMachineConfiguration) (*VirtualMachine, er state: VirtualMachineState(0), stateNotify: infinity.NewChannel[VirtualMachineState](), } - stateHandle := cgo.NewHandle(machineState) + + disconnectedIn := infinity.NewChannel[*disconnected]() + disconnectedOut := infinity.NewChannel[*DisconnectedError]() + disconnectedHandle := cgo.NewHandle(disconnectedIn) + v := &VirtualMachine{ id: cs.String(), pointer: objc.NewPointer( @@ -132,11 +144,14 @@ func NewVirtualMachine(config *VirtualMachineConfiguration) (*VirtualMachine, er objc.Ptr(config), dispatchQueue, C.uintptr_t(stateHandle), + C.uintptr_t(disconnectedHandle), ), ), - dispatchQueue: dispatchQueue, - machineState: machineState, - config: config, + dispatchQueue: dispatchQueue, + machineState: machineState, + disconnectedIn: disconnectedIn, + disconnectedOut: disconnectedOut, + config: config, } objc.SetFinalizer(v, func(self *VirtualMachine) { @@ -357,3 +372,85 @@ func (v *VirtualMachine) StartGraphicApplication(width, height float64) error { C.startVirtualMachineWindow(objc.Ptr(v), C.double(width), C.double(height)) return nil } + +// DisconnectedError represents an error that occurs when a VM’s network attachment is disconnected +// due to a network-related issue. This error is triggered by the framework when such a disconnection happens. +type DisconnectedError struct { + // Err is the underlying error that caused the disconnection, triggered by the framework. + // This error provides information on why the network attachment was disconnected. + Err error + // The network device configuration associated with the disconnection event. + // This configuration helps identify which network device experienced the disconnection. + // If Config is nil, the specific configuration details are unavailable. + Config *VirtioNetworkDeviceConfiguration +} + +var _ error = (*DisconnectedError)(nil) + +func (e *DisconnectedError) Unwrap() error { return e.Err } +func (e *DisconnectedError) Error() string { + if e.Config == nil { + return e.Err.Error() + } + return fmt.Sprintf("%s: %v", e.Config.attachment, e.Err) +} + +type disconnected struct { + err error + index int +} + +// NetworkDeviceAttachmentWasDisconnected returns a receive channel. +// The channel emits an error message each time the network attachment is disconnected, +// typically triggered by events such as failure to start, initial boot, device reset, or reboot. +// As a result, this method may be invoked multiple times throughout the virtual machine's lifecycle. +// +// This is only supported on macOS 12 and newer, error will be returned on older versions. +func (v *VirtualMachine) NetworkDeviceAttachmentWasDisconnected() (<-chan *DisconnectedError, error) { + if err := macOSAvailable(12); err != nil { + return nil, err + } + v.watchDisconnectedOnce.Do(func() { + go v.watchDisconnected() + }) + return v.disconnectedOut.Out(), nil +} + +// TODO(codehex): refactoring to leave using machineState's mutex lock. +func (v *VirtualMachine) watchDisconnected() { + for disconnected := range v.disconnectedIn.Out() { + v.mu.RLock() + config := sliceutil.FindValueByIndex( + v.config.networkDeviceConfiguration, + disconnected.index, + ) + v.mu.RUnlock() + v.disconnectedOut.In() <- &DisconnectedError{ + Err: disconnected.err, + Config: config, + } + } + v.disconnectedOut.Close() +} + +//export emitAttachmentWasDisconnected +func emitAttachmentWasDisconnected(index C.int, errPtr unsafe.Pointer, cgoHandleUintptr C.uintptr_t) { + handler := cgo.Handle(cgoHandleUintptr) + err := newNSError(errPtr) + // I expected it will not cause panic. + // if caused panic, that's unexpected behavior. + ch, _ := handler.Value().(*infinity.Channel[*disconnected]) + ch.In() <- &disconnected{ + err: err, + index: int(index), + } +} + +//export closeAttachmentWasDisconnectedChannel +func closeAttachmentWasDisconnectedChannel(cgoHandleUintptr C.uintptr_t) { + handler := cgo.Handle(cgoHandleUintptr) + // I expected it will not cause panic. + // if caused panic, that's unexpected behavior. + ch, _ := handler.Value().(*infinity.Channel[*disconnected]) + ch.Close() +} diff --git a/virtualization_11.h b/virtualization_11.h index 95f2f532..1829286a 100644 --- a/virtualization_11.h +++ b/virtualization_11.h @@ -13,11 +13,25 @@ void connectionHandler(void *connection, void *err, uintptr_t cgoHandle); void changeStateOnObserver(int state, uintptr_t cgoHandle); bool shouldAcceptNewConnectionHandler(uintptr_t cgoHandle, void *connection, void *socketDevice); +void emitAttachmentWasDisconnected(int index, void *err, uintptr_t cgoHandle); +void closeAttachmentWasDisconnectedChannel(uintptr_t cgoHandle); @interface Observer : NSObject - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context; @end +@interface VZVirtualMachineDelegateWrapper : NSObject +@property (nonatomic, strong, readonly) NSHashTable> *delegates; + +- (instancetype)init; +- (void)addDelegate:(id)delegate; +- (void)guestDidStopVirtualMachine:(VZVirtualMachine *)virtualMachine; +- (void)virtualMachine:(VZVirtualMachine *)virtualMachine didStopWithError:(NSError *)error; +- (void)virtualMachine:(VZVirtualMachine *)virtualMachine + networkDevice:(VZNetworkDevice *)networkDevice + attachmentWasDisconnectedWithError:(NSError *)error API_AVAILABLE(macos(12.0)); +@end + @interface ObservableVZVirtualMachine : VZVirtualMachine - (instancetype)initWithConfiguration:(VZVirtualMachineConfiguration *)configuration queue:(dispatch_queue_t)queue @@ -25,6 +39,16 @@ bool shouldAcceptNewConnectionHandler(uintptr_t cgoHandle, void *connection, voi - (void)dealloc; @end +@interface NetworkDeviceDisconnectedHandler : NSObject +- (instancetype)initWithHandle:(uintptr_t)cgoHandle; +- (void)virtualMachine:(VZVirtualMachine *)virtualMachine + networkDevice:(VZNetworkDevice *)networkDevice + attachmentWasDisconnectedWithError:(NSError *)error API_AVAILABLE(macos(12.0)); +- (int)networkDevices:(NSArray *)networkDevices + indexOf:(VZNetworkDevice *)networkDevice API_AVAILABLE(macos(12.0)); +- (void)dealloc; +@end + /* VZVirtioSocketListener */ @interface VZVirtioSocketListenerDelegateImpl : NSObject - (instancetype)initWithHandle:(uintptr_t)cgoHandle; @@ -88,7 +112,7 @@ void VZVirtioSocketDevice_removeSocketListenerForPort(void *socketDevice, void * void VZVirtioSocketDevice_connectToPort(void *socketDevice, void *vmQueue, uint32_t port, uintptr_t cgoHandle); /* VirtualMachine */ -void *newVZVirtualMachineWithDispatchQueue(void *config, void *queue, uintptr_t cgoHandle); +void *newVZVirtualMachineWithDispatchQueue(void *config, void *queue, uintptr_t statusUpdateCgoHandle, uintptr_t disconnectedCgoHandle); bool requestStopVirtualMachine(void *machine, void *queue, void **error); void startWithCompletionHandler(void *machine, void *queue, uintptr_t cgoHandle); void pauseWithCompletionHandler(void *machine, void *queue, uintptr_t cgoHandle); diff --git a/virtualization_11.m b/virtualization_11.m index d2d898fd..017f6f70 100644 --- a/virtualization_11.m +++ b/virtualization_11.m @@ -17,26 +17,126 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N } @end +@implementation VZVirtualMachineDelegateWrapper +- (instancetype)init +{ + self = [super init]; + if (self) { + _delegates = [NSHashTable weakObjectsHashTable]; + } + return self; +} + +- (void)addDelegate:(id)delegate +{ + [self.delegates addObject:delegate]; +} + +- (void)guestDidStopVirtualMachine:(VZVirtualMachine *)virtualMachine +{ + for (id delegate in self.delegates) { + if ([delegate respondsToSelector:@selector(guestDidStopVirtualMachine:)]) { + [delegate guestDidStopVirtualMachine:virtualMachine]; + } + } +} + +- (void)virtualMachine:(VZVirtualMachine *)virtualMachine didStopWithError:(NSError *)error +{ + for (id delegate in self.delegates) { + if ([delegate respondsToSelector:@selector(virtualMachine:didStopWithError:)]) { + [delegate virtualMachine:virtualMachine didStopWithError:error]; + } + } +} + +- (void)virtualMachine:(VZVirtualMachine *)virtualMachine networkDevice:(VZNetworkDevice *)networkDevice + attachmentWasDisconnectedWithError:(NSError *)error +{ + for (id delegate in self.delegates) { + if ([delegate respondsToSelector:@selector(virtualMachine:networkDevice:attachmentWasDisconnectedWithError:)]) { + [delegate virtualMachine:virtualMachine networkDevice:networkDevice attachmentWasDisconnectedWithError:error]; + } + } +} +@end + +@implementation NetworkDeviceDisconnectedHandler { + uintptr_t _cgoHandle; +} + +- (instancetype)initWithHandle:(uintptr_t)cgoHandle +{ + self = [super init]; + if (self) { + _cgoHandle = cgoHandle; + } + return self; +} + +- (void)virtualMachine:(VZVirtualMachine *)virtualMachine + networkDevice:(VZNetworkDevice *)networkDevice + attachmentWasDisconnectedWithError:(NSError *)error +{ + int index = [self networkDevices:virtualMachine.networkDevices indexOf:networkDevice]; + emitAttachmentWasDisconnected(index, error, _cgoHandle); +} + +- (int)networkDevices:(NSArray *)networkDevices + indexOf:(VZNetworkDevice *)networkDevice +{ + NSInteger index = [networkDevices indexOfObject:networkDevice]; + if (index != NSNotFound) { + return (int)index; + } + return -1; +} + +- (void)dealloc +{ + closeAttachmentWasDisconnectedChannel(_cgoHandle); + [super dealloc]; +} +@end + @implementation ObservableVZVirtualMachine { Observer *_observer; + VZVirtualMachineDelegateWrapper *_delegateWrapper; }; - (instancetype)initWithConfiguration:(VZVirtualMachineConfiguration *)configuration queue:(dispatch_queue_t)queue statusUpdateHandle:(uintptr_t)statusUpdateHandle { self = [super initWithConfiguration:configuration queue:queue]; - _observer = [[Observer alloc] init]; - [self addObserver:_observer - forKeyPath:@"state" - options:NSKeyValueObservingOptionNew - context:(void *)statusUpdateHandle]; + if (self) { + _observer = [[Observer alloc] init]; + [self addObserver:_observer + forKeyPath:@"state" + options:NSKeyValueObservingOptionNew + context:(void *)statusUpdateHandle]; + _delegateWrapper = [[VZVirtualMachineDelegateWrapper alloc] init]; + [super setDelegate:_delegateWrapper]; + } return self; } +- (void)setDelegate:(id)delegate +{ + if (delegate != _delegateWrapper) { + [_delegateWrapper addDelegate:delegate]; + } +} + +- (id)delegate +{ + return _delegateWrapper; +} + - (void)dealloc { [self removeObserver:_observer forKeyPath:@"state"]; [_observer release]; + [_delegateWrapper release]; [super dealloc]; } @end @@ -48,7 +148,9 @@ @implementation VZVirtioSocketListenerDelegateImpl { - (instancetype)initWithHandle:(uintptr_t)cgoHandle { self = [super init]; - _cgoHandle = cgoHandle; + if (self) { + _cgoHandle = cgoHandle; + } return self; } @@ -731,13 +833,15 @@ VZVirtioSocketConnectionFlat convertVZVirtioSocketConnection2Flat(void *connecti Every operation on the virtual machine must be done on that queue. The callbacks and delegate methods are invoked on that queue. If the queue is not serial, the behavior is undefined. */ -void *newVZVirtualMachineWithDispatchQueue(void *config, void *queue, uintptr_t cgoHandle) +void *newVZVirtualMachineWithDispatchQueue(void *config, void *queue, uintptr_t statusUpdateCgoHandle, uintptr_t disconnectedCgoHandle) { if (@available(macOS 11, *)) { ObservableVZVirtualMachine *vm = [[ObservableVZVirtualMachine alloc] initWithConfiguration:(VZVirtualMachineConfiguration *)config queue:(dispatch_queue_t)queue - statusUpdateHandle:cgoHandle]; + statusUpdateHandle:statusUpdateCgoHandle]; + NetworkDeviceDisconnectedHandler *delegate = [[NetworkDeviceDisconnectedHandler alloc] initWithHandle:disconnectedCgoHandle]; + [vm setDelegate:[delegate autorelease]]; return vm; } diff --git a/virtualization_14.h b/virtualization_14.h index 5ee272df..c962269e 100644 --- a/virtualization_14.h +++ b/virtualization_14.h @@ -24,6 +24,6 @@ void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *url, double tim @interface VZNetworkBlockDeviceStorageDeviceAttachmentDelegateImpl : NSObject - (instancetype)initWithHandle:(uintptr_t)cgoHandle; -- (void)attachment:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment didEncounterError:(NSError *)error; -- (void)attachmentWasConnected:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment; +- (void)attachment:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment didEncounterError:(NSError *)error API_AVAILABLE(macos(14.0)); +- (void)attachmentWasConnected:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment API_AVAILABLE(macos(14.0)); @end diff --git a/virtualization_test.go b/virtualization_test.go index d56a736c..2a1f3e7d 100644 --- a/virtualization_test.go +++ b/virtualization_test.go @@ -3,6 +3,7 @@ package vz_test import ( "errors" "fmt" + "log" "math" "net" "os" @@ -172,6 +173,16 @@ func newVirtualizationMachine( t.Fatal("want CanStart is true") } + // As it is not possible to intentionally cause errors, only logging is possible. + disconnected, err := vm.NetworkDeviceAttachmentWasDisconnected() + if err == nil { + go func() { + for disconnected := range disconnected { + log.Println(disconnected) + } + }() + } + if err := vm.Start(); err != nil { t.Fatal(err) } @@ -187,6 +198,10 @@ func newVirtualizationMachine( // does not seem to have a connection timeout set. if vz.Available(12) { time.Sleep(5 * time.Second) + } else { + // Not immediately available in the x86_64 environment + // so wait a little longer for the orbit before testing. + time.Sleep(3 * time.Second) } const max = 5 diff --git a/virtualization_view.m b/virtualization_view.m index 7ff298b2..9fbf6d18 100644 --- a/virtualization_view.m +++ b/virtualization_view.m @@ -178,7 +178,7 @@ - (instancetype)initWithVirtualMachine:(VZVirtualMachine *)virtualMachine { self = [super init]; _virtualMachine = virtualMachine; - _virtualMachine.delegate = self; + [_virtualMachine setDelegate:self]; // Setup virtual machine view configs VZVirtualMachineView *view = [[[VZVirtualMachineView alloc] init] autorelease];