Skip to content

Commit

Permalink
Merge pull request #169 from Code-Hex/add/attachmentWasDisconnectedWi…
Browse files Browse the repository at this point in the history
…thError

added NetworkDeviceAttachmentWasDisconnected api
  • Loading branch information
Code-Hex authored Nov 5, 2024
2 parents 46a19ee + 4be99b6 commit d2ef438
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 34 deletions.
12 changes: 3 additions & 9 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type VirtualMachineConfiguration struct {
memorySize uint64
*pointer

networkDeviceConfiguration []*VirtioNetworkDeviceConfiguration
storageDeviceConfiguration []StorageDeviceConfiguration
}

Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions internal/sliceutil/sliceutil.go
Original file line number Diff line number Diff line change
@@ -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]
}
50 changes: 50 additions & 0 deletions internal/sliceutil/sliceutil_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
35 changes: 26 additions & 9 deletions network.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"net"
"os"
"syscall"
"unsafe"

"github.com/Code-Hex/vz/v3/internal/objc"
)
Expand Down Expand Up @@ -91,6 +90,10 @@ type NATNetworkDeviceAttachment struct {
*baseNetworkDeviceAttachment
}

func (*NATNetworkDeviceAttachment) String() string {
return "NATNetworkDeviceAttachment"
}

var _ NetworkDeviceAttachment = (*NATNetworkDeviceAttachment)(nil)

// NewNATNetworkDeviceAttachment creates a new NATNetworkDeviceAttachment.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
}

Expand All @@ -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.
Expand All @@ -282,27 +295,31 @@ 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,
}
}

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 {
Expand Down
105 changes: 101 additions & 4 deletions virtualization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -123,20 +131,27 @@ 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(
C.newVZVirtualMachineWithDispatchQueue(
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) {
Expand Down Expand Up @@ -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()
}
26 changes: 25 additions & 1 deletion virtualization_11.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,42 @@
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 <VZVirtualMachineDelegate>
@property (nonatomic, strong, readonly) NSHashTable<id<VZVirtualMachineDelegate>> *delegates;

- (instancetype)init;
- (void)addDelegate:(id<VZVirtualMachineDelegate>)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
statusUpdateHandle:(uintptr_t)statusUpdateHandle;
- (void)dealloc;
@end

@interface NetworkDeviceDisconnectedHandler : NSObject <VZVirtualMachineDelegate>
- (instancetype)initWithHandle:(uintptr_t)cgoHandle;
- (void)virtualMachine:(VZVirtualMachine *)virtualMachine
networkDevice:(VZNetworkDevice *)networkDevice
attachmentWasDisconnectedWithError:(NSError *)error API_AVAILABLE(macos(12.0));
- (int)networkDevices:(NSArray<VZNetworkDevice *> *)networkDevices
indexOf:(VZNetworkDevice *)networkDevice API_AVAILABLE(macos(12.0));
- (void)dealloc;
@end

/* VZVirtioSocketListener */
@interface VZVirtioSocketListenerDelegateImpl : NSObject <VZVirtioSocketListenerDelegate>
- (instancetype)initWithHandle:(uintptr_t)cgoHandle;
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit d2ef438

Please sign in to comment.