diff --git a/configuration.go b/configuration.go index e59aa82f..4a7ea3f7 100644 --- a/configuration.go +++ b/configuration.go @@ -37,6 +37,8 @@ type VirtualMachineConfiguration struct { cpuCount uint memorySize uint64 *pointer + + storageDeviceConfiguration []StorageDeviceConfiguration } // NewVirtualMachineConfiguration creates a new configuration. @@ -172,6 +174,13 @@ func (v *VirtualMachineConfiguration) SetStorageDevicesVirtualMachineConfigurati } array := objc.ConvertToNSMutableArray(ptrs) C.setStorageDevicesVZVirtualMachineConfiguration(objc.Ptr(v), objc.Ptr(array)) + v.storageDeviceConfiguration = cs +} + +// StorageDevices return the list of storage device configuration configured in this virtual machine configuration. +// Return an empty array if no storage device configuration is set. +func (v *VirtualMachineConfiguration) StorageDevices() []StorageDeviceConfiguration { + return v.storageDeviceConfiguration } // SetDirectorySharingDevicesVirtualMachineConfiguration sets list of directory sharing devices. Empty by default. diff --git a/example/macOS/main.go b/example/macOS/main.go index 897702db..b97c0999 100644 --- a/example/macOS/main.go +++ b/example/macOS/main.go @@ -78,6 +78,21 @@ func runVM(ctx context.Context) error { } }() + // it start listening to the NBD server, if any + nbdAttachment := retrieveNetworkBlockDeviceStorageDeviceAttachment(config.StorageDevices()) + if nbdAttachment != nil { + go func() { + for { + select { + case err := <-nbdAttachment.DidEncounterError(): + log.Printf("NBD client has been encountered error: %v\n", err) + case <-nbdAttachment.Connected(): + log.Println("NBD client connected with the server") + } + } + }() + } + // cleanup is this function is useful when finished graphic application. cleanup := func() { for i := 1; vm.CanRequestStop(); i++ { @@ -102,9 +117,7 @@ func runVM(ctx context.Context) error { log.Println("finished cleanup") } - runtime.LockOSThread() vm.StartGraphicApplication(960, 600) - runtime.UnlockOSThread() cleanup() @@ -144,29 +157,30 @@ func computeMemorySize() uint64 { } func createBlockDeviceConfiguration(diskPath string) (*vz.VirtioBlockDeviceConfiguration, error) { - var attachment vz.StorageDeviceAttachment - var err error - - if nbdURL == "" { - // create disk image with 64 GiB - if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil { - if !os.IsExist(err) { - return nil, fmt.Errorf("failed to create disk image: %w", err) - } + // create disk image with 64 GiB + if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil { + if !os.IsExist(err) { + return nil, fmt.Errorf("failed to create disk image: %w", err) } + } - attachment, err = vz.NewDiskImageStorageDeviceAttachment( - diskPath, - false, - ) - } else { - attachment, err = vz.NewNetworkBlockDeviceStorageDeviceAttachment( - nbdURL, - 10*time.Second, - false, - vz.DiskSynchronizationModeFull, - ) + attachment, err := vz.NewDiskImageStorageDeviceAttachment( + diskPath, + false, + ) + if err != nil { + return nil, err } + return vz.NewVirtioBlockDeviceConfiguration(attachment) +} + +func createNetworkBlockDeviceConfiguration(nbdURL string) (*vz.VirtioBlockDeviceConfiguration, error) { + attachment, err := vz.NewNetworkBlockDeviceStorageDeviceAttachment( + nbdURL, + 10*time.Second, + false, + vz.DiskSynchronizationModeFull, + ) if err != nil { return nil, err } @@ -277,7 +291,15 @@ func setupVMConfiguration(platformConfig vz.PlatformConfiguration) (*vz.VirtualM if err != nil { return nil, fmt.Errorf("failed to create block device configuration: %w", err) } - config.SetStorageDevicesVirtualMachineConfiguration([]vz.StorageDeviceConfiguration{blockDeviceConfig}) + sdconfigs := []vz.StorageDeviceConfiguration{blockDeviceConfig} + if nbdURL != "" { + ndbConfig, err := createNetworkBlockDeviceConfiguration(nbdURL) + if err != nil { + return nil, fmt.Errorf("failed to create network block device configuration: %w", err) + } + sdconfigs = append(sdconfigs, ndbConfig) + } + config.SetStorageDevicesVirtualMachineConfiguration(sdconfigs) networkDeviceConfig, err := createNetworkDeviceConfiguration() if err != nil { @@ -331,3 +353,13 @@ func setupVMConfiguration(platformConfig vz.PlatformConfiguration) (*vz.VirtualM return config, nil } + +func retrieveNetworkBlockDeviceStorageDeviceAttachment(storages []vz.StorageDeviceConfiguration) *vz.NetworkBlockDeviceStorageDeviceAttachment { + for _, storage := range storages { + attachment := storage.Attachment() + if nbdAttachment, ok := attachment.(*vz.NetworkBlockDeviceStorageDeviceAttachment); ok { + return nbdAttachment + } + } + return nil +} diff --git a/storage.go b/storage.go index f1c51c43..56701092 100644 --- a/storage.go +++ b/storage.go @@ -12,8 +12,11 @@ package vz import "C" import ( "os" + "runtime/cgo" "time" + "unsafe" + infinity "github.com/Code-Hex/go-infinity-channel" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -151,11 +154,17 @@ type StorageDeviceConfiguration interface { objc.NSObject storageDeviceConfiguration() + Attachment() StorageDeviceAttachment } -type baseStorageDeviceConfiguration struct{} +type baseStorageDeviceConfiguration struct { + attachment StorageDeviceAttachment +} func (*baseStorageDeviceConfiguration) storageDeviceConfiguration() {} +func (b *baseStorageDeviceConfiguration) Attachment() StorageDeviceAttachment { + return b.attachment +} var _ StorageDeviceConfiguration = (*VirtioBlockDeviceConfiguration)(nil) @@ -192,6 +201,9 @@ func NewVirtioBlockDeviceConfiguration(attachment StorageDeviceAttachment) (*Vir objc.Ptr(attachment), ), ), + baseStorageDeviceConfiguration: &baseStorageDeviceConfiguration{ + attachment: attachment, + }, } objc.SetFinalizer(config, func(self *VirtioBlockDeviceConfiguration) { objc.Release(self) @@ -250,10 +262,6 @@ type USBMassStorageDeviceConfiguration struct { *pointer *baseStorageDeviceConfiguration - - // marking as currently reachable. - // This ensures that the object is not freed, and its finalizer is not run - attachment StorageDeviceAttachment } // NewUSBMassStorageDeviceConfiguration initialize a USBMassStorageDeviceConfiguration @@ -269,7 +277,9 @@ func NewUSBMassStorageDeviceConfiguration(attachment StorageDeviceAttachment) (* pointer: objc.NewPointer( C.newVZUSBMassStorageDeviceConfiguration(objc.Ptr(attachment)), ), - attachment: attachment, + baseStorageDeviceConfiguration: &baseStorageDeviceConfiguration{ + attachment: attachment, + }, } objc.SetFinalizer(usbMass, func(self *USBMassStorageDeviceConfiguration) { objc.Release(self) @@ -284,10 +294,6 @@ type NVMExpressControllerDeviceConfiguration struct { *pointer *baseStorageDeviceConfiguration - - // marking as currently reachable. - // This ensures that the object is not freed, and its finalizer is not run - attachment StorageDeviceAttachment } // NewNVMExpressControllerDeviceConfiguration creates a new NVMExpressControllerDeviceConfiguration with @@ -306,7 +312,9 @@ func NewNVMExpressControllerDeviceConfiguration(attachment StorageDeviceAttachme pointer: objc.NewPointer( C.newVZNVMExpressControllerDeviceConfiguration(objc.Ptr(attachment)), ), - attachment: attachment, + baseStorageDeviceConfiguration: &baseStorageDeviceConfiguration{ + attachment: attachment, + }, } objc.SetFinalizer(nvmExpress, func(self *NVMExpressControllerDeviceConfiguration) { objc.Release(self) @@ -411,6 +419,9 @@ type NetworkBlockDeviceStorageDeviceAttachment struct { *pointer *baseStorageDeviceAttachment + + didEncounterError *infinity.Channel[error] + connected *infinity.Channel[struct{}] } var _ StorageDeviceAttachment = (*NetworkBlockDeviceStorageDeviceAttachment)(nil) @@ -419,6 +430,9 @@ var _ StorageDeviceAttachment = (*NetworkBlockDeviceStorageDeviceAttachment)(nil // Uniform Resource Indicator (URI) represented as a URL, timeout value, and read-only and synchronization modes // that you provide. // +// It also set up a channel that will be used by the VZNetworkBlockDeviceStorageDeviceAttachmentDelegate to +// return changes to the NetworkBlockDeviceAttachment +// // - url is the NBD server URI. The format specified by https://github.com/NetworkBlockDevice/nbd/blob/master/doc/uri.md // - timeout is the duration for the connection between the client and server. When the timeout expires, an attempt to reconnect with the server takes place. // - forcedReadOnly if true forces the disk attachment to be read-only, regardless of whether or not the NBD server supports write requests. @@ -431,6 +445,17 @@ func NewNetworkBlockDeviceStorageDeviceAttachment(url string, timeout time.Durat return nil, err } + didEncounterError := infinity.NewChannel[error]() + connected := infinity.NewChannel[struct{}]() + + handle := cgo.NewHandle(func(err error) { + if err != nil { + didEncounterError.In() <- err + return + } + connected.In() <- struct{}{} + }) + nserrPtr := newNSErrorAsNil() urlChar := charWithGoString(url) @@ -443,8 +468,11 @@ func NewNetworkBlockDeviceStorageDeviceAttachment(url string, timeout time.Durat C.bool(forcedReadOnly), C.int(syncMode), &nserrPtr, + C.uintptr_t(handle), ), ), + didEncounterError: didEncounterError, + connected: connected, } if err := newNSError(nserrPtr); err != nil { return nil, err @@ -454,3 +482,49 @@ func NewNetworkBlockDeviceStorageDeviceAttachment(url string, timeout time.Durat }) return attachment, nil } + +// Connected receive the signal via channel when the NBD client successfully connects or reconnects with the server. +// +// The NBD connection with the server takes place when the VM is first started, and reconnection attempts take place when the connection +// times out or when the NBD client has encountered a recoverable error, such as an I/O error from the server. +// +// Note that the Virtualization framework may call this method multiple times during a VM’s life cycle. Reconnections are transparent to the guest. +func (n *NetworkBlockDeviceStorageDeviceAttachment) Connected() <-chan struct{} { + return n.connected.Out() +} + +// The DidEncounterError is triggered via the channel when the NBD client encounters an error that cannot be resolved on the client side. +// In this state, the client will continue attempting to reconnect, but recovery depends entirely on the server's availability. +// If the server resumes operation, the connection will recover automatically; however, until the server is restored, the client will continue to experience errors. +func (n *NetworkBlockDeviceStorageDeviceAttachment) DidEncounterError() <-chan error { + return n.didEncounterError.Out() +} + +// attachmentDidEncounterErrorHandler function is called when the NBD client encounters a nonrecoverable error. +// After the attachment object calls this method, the NBD client is in a nonfunctional state. +// +//export attachmentDidEncounterErrorHandler +func attachmentDidEncounterErrorHandler(cgoHandleUintptr C.uintptr_t, errorPtr unsafe.Pointer) { + cgoHandle := cgo.Handle(cgoHandleUintptr) + handler := cgoHandle.Value().(func(error)) + + err := newNSError(errorPtr) + + handler(err) +} + +// attachmentWasConnectedHandler function is called when a connection to the server is first established as the VM starts, +// and during any reconnection attempts triggered by connection timeouts or recoverable errors encountered by the NBD client, +// such as server-side I/O errors. +// +// Note that the Virtualization framework may invoke this method multiple times throughout the VM’s lifecycle, +// ensuring reconnection processes remain seamless and transparent to the guest. +// For more details, see: https://developer.apple.com/documentation/virtualization/vznetworkblockdevicestoragedeviceattachmentdelegate/4168511-attachmentwasconnected?language=objc +// +//export attachmentWasConnectedHandler +func attachmentWasConnectedHandler(cgoHandleUintptr C.uintptr_t) { + cgoHandle := cgo.Handle(cgoHandleUintptr) + handler := cgoHandle.Value().(func(error)) + + handler(nil) +} diff --git a/virtualization_11.h b/virtualization_11.h index f5b6aabd..95f2f532 100644 --- a/virtualization_11.h +++ b/virtualization_11.h @@ -59,6 +59,7 @@ void setSocketDevicesVZVirtualMachineConfiguration(void *config, void *socketDevicesVZVirtualMachineConfiguration(void *config); void setStorageDevicesVZVirtualMachineConfiguration(void *config, void *storageDevices); +void *storageDevicesVZVirtualMachineConfiguration(void *config); /* Configurations */ void *newVZFileHandleSerialPortAttachment(int readFileDescriptor, int writeFileDescriptor); diff --git a/virtualization_11.m b/virtualization_11.m index 3355a33c..d2d898fd 100644 --- a/virtualization_11.m +++ b/virtualization_11.m @@ -327,6 +327,18 @@ void setStorageDevicesVZVirtualMachineConfiguration(void *config, RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } +/*! + @abstract Return the list of storage devices configurations for this VZVirtualMachineConfiguration. Return an empty array if no storage device configuration is set. + */ +void *storageDevicesVZVirtualMachineConfiguration(void *config) +{ + if (@available(macOS 11, *)) { + return [(VZVirtualMachineConfiguration *)config storageDevices]; // NSArray + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + /*! @abstract Intialize the VZFileHandleSerialPortAttachment from file descriptors. @param readFileDescriptor File descriptor for reading from the file. diff --git a/virtualization_14.h b/virtualization_14.h index c8abb273..5ee272df 100644 --- a/virtualization_14.h +++ b/virtualization_14.h @@ -13,7 +13,17 @@ #import "virtualization_helper.h" #import +/* exported from cgo */ +void attachmentDidEncounterErrorHandler(uintptr_t cgoHandle, void *err); +void attachmentWasConnectedHandler(uintptr_t cgoHandle); + /* macOS 14 API */ void *newVZNVMExpressControllerDeviceConfiguration(void *attachment); void *newVZDiskBlockDeviceStorageDeviceAttachment(int fileDescriptor, bool readOnly, int syncMode, void **error); -void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *url, double timeout, bool forcedReadOnly, int syncMode, void **error); \ No newline at end of file +void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *url, double timeout, bool forcedReadOnly, int syncMode, void **error, uintptr_t cgoHandle); + +@interface VZNetworkBlockDeviceStorageDeviceAttachmentDelegateImpl : NSObject +- (instancetype)initWithHandle:(uintptr_t)cgoHandle; +- (void)attachment:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment didEncounterError:(NSError *)error; +- (void)attachmentWasConnected:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment; +@end diff --git a/virtualization_14.m b/virtualization_14.m index f528518a..9fbe6e46 100644 --- a/virtualization_14.m +++ b/virtualization_14.m @@ -68,19 +68,47 @@ Setting forcedReadOnly to YES forces the NBD client to show up as read-only to the guest regardless of whether or not the NBD server advertises itself as read-only. */ -void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *uri, double timeout, bool forcedReadOnly, int syncMode, void **error) +void *newVZNetworkBlockDeviceStorageDeviceAttachment(const char *uri, double timeout, bool forcedReadOnly, int syncMode, void **error, uintptr_t cgoHandle) { #ifdef INCLUDE_TARGET_OSX_14 if (@available(macOS 14, *)) { NSURL *url = [NSURL URLWithString:[NSString stringWithUTF8String:uri]]; - return [[VZNetworkBlockDeviceStorageDeviceAttachment alloc] + VZNetworkBlockDeviceStorageDeviceAttachment *attachment = [[VZNetworkBlockDeviceStorageDeviceAttachment alloc] initWithURL:url timeout:(NSTimeInterval)timeout forcedReadOnly:(BOOL)forcedReadOnly synchronizationMode:(VZDiskSynchronizationMode)syncMode error:(NSError *_Nullable *_Nullable)error]; + + if (attachment) { + [attachment setDelegate:[[[VZNetworkBlockDeviceStorageDeviceAttachmentDelegateImpl alloc] initWithHandle:cgoHandle] autorelease]]; + } + + return attachment; } #endif RAISE_UNSUPPORTED_MACOS_EXCEPTION(); -} \ No newline at end of file +} + +@implementation VZNetworkBlockDeviceStorageDeviceAttachmentDelegateImpl { + uintptr_t _cgoHandle; +} + +- (instancetype)initWithHandle:(uintptr_t)cgoHandle +{ + self = [super init]; + _cgoHandle = cgoHandle; + return self; +} + +- (void)attachment:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment didEncounterError:(NSError *)error +{ + attachmentDidEncounterErrorHandler(_cgoHandle, error); +} + +- (void)attachmentWasConnected:(VZNetworkBlockDeviceStorageDeviceAttachment *)attachment +{ + attachmentWasConnectedHandler(_cgoHandle); +} +@end diff --git a/virtualization_15.m b/virtualization_15.m index 29552673..b5c942f1 100644 --- a/virtualization_15.m +++ b/virtualization_15.m @@ -11,7 +11,7 @@ bool isNestedVirtualizationSupported() { #ifdef INCLUDE_TARGET_OSX_15 if (@available(macOS 15, *)) { - return (bool) VZGenericPlatformConfiguration.isNestedVirtualizationSupported; + return (bool)VZGenericPlatformConfiguration.isNestedVirtualizationSupported; } #endif RAISE_UNSUPPORTED_MACOS_EXCEPTION(); @@ -25,7 +25,7 @@ void setNestedVirtualizationEnabled(void *config, bool nestedVirtualizationEnabl #ifdef INCLUDE_TARGET_OSX_15 if (@available(macOS 15, *)) { VZGenericPlatformConfiguration *platformConfig = (VZGenericPlatformConfiguration *)config; - platformConfig.nestedVirtualizationEnabled = (BOOL) nestedVirtualizationEnabled; + platformConfig.nestedVirtualizationEnabled = (BOOL)nestedVirtualizationEnabled; return; } #endif