diff --git a/shared_directory.go b/shared_directory.go index 8b628890..795b9343 100644 --- a/shared_directory.go +++ b/shared_directory.go @@ -10,6 +10,7 @@ package vz import "C" import ( "os" + "unsafe" "github.com/Code-Hex/vz/v3/internal/objc" ) @@ -182,3 +183,80 @@ func MacOSGuestAutomountTag() (string, error) { cstring := (*char)(C.getMacOSGuestAutomountTag()) return cstring.String(), nil } + +// VirtioFileSystemDevice is a runtime Virtio file system device obtained from a running +// VirtualMachine via DirectorySharingDevices. Unlike VirtioFileSystemDeviceConfiguration +// (a configuration object set before start), this is the live device: its directory share +// can be swapped while the VM runs (macOS 12+), so the host can add or remove the +// directories a guest sees without recreating the VM. +// +// Don't create a VirtioFileSystemDevice directly. Request a file system device in your +// configuration and obtain the running instance via VirtualMachine.DirectorySharingDevices. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdevice?language=objc +type VirtioFileSystemDevice struct { + dispatchQueue unsafe.Pointer + *pointer +} + +func newVirtioFileSystemDevice(ptr, dispatchQueue unsafe.Pointer) *VirtioFileSystemDevice { + return &VirtioFileSystemDevice{ + dispatchQueue: dispatchQueue, + pointer: objc.NewPointer(ptr), + } +} + +// SetShare swaps the directory share on this running device. The mutation runs on the +// VM's serial dispatch queue, as the framework requires for every VZVirtualMachine call. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdevice/share?language=objc +func (d *VirtioFileSystemDevice) SetShare(share DirectoryShare) { + C.setShareVZVirtioFileSystemDevice(objc.Ptr(d), objc.Ptr(share), d.dispatchQueue) +} + +// Share returns the directory share currently set on this running device, or nil if none +// is set. The concrete type is *SingleDirectoryShare or *MultipleDirectoryShare, matching +// what was last passed to SetShare or set in the configuration. The read runs on the VM's +// serial dispatch queue. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtiofilesystemdevice/share?language=objc +func (d *VirtioFileSystemDevice) Share() DirectoryShare { + ptr := C.getShareVZVirtioFileSystemDevice(objc.Ptr(d), d.dispatchQueue) + if ptr == nil { + return nil + } + if bool(C.isMultipleDirectoryShare(ptr)) { + share := &MultipleDirectoryShare{pointer: objc.NewPointer(ptr)} + objc.SetFinalizer(share, func(self *MultipleDirectoryShare) { + objc.Release(self) + }) + return share + } + share := &SingleDirectoryShare{pointer: objc.NewPointer(ptr)} + objc.SetFinalizer(share, func(self *SingleDirectoryShare) { + objc.Release(self) + }) + return share +} + +// DirectorySharingDevices returns the list of directory sharing devices configured on this +// running virtual machine, or an empty slice if none is configured. Since vz only creates +// VirtioFileSystemDevices, every element is one. +// +// This is only supported on macOS 12 and newer; nil is returned on older versions. +// +// see: https://developer.apple.com/documentation/virtualization/vzvirtualmachine/directorysharingdevices?language=objc +func (v *VirtualMachine) DirectorySharingDevices() []*VirtioFileSystemDevice { + if err := macOSAvailable(12); err != nil { + return nil + } + nsArray := objc.NewNSArray( + C.VZVirtualMachine_directorySharingDevices(objc.Ptr(v)), + ) + ptrs := nsArray.ToPointerSlice() + devices := make([]*VirtioFileSystemDevice, len(ptrs)) + for i, ptr := range ptrs { + devices[i] = newVirtioFileSystemDevice(ptr, v.dispatchQueue) + } + return devices +} diff --git a/shared_directory_test.go b/shared_directory_test.go index ed02464a..3b2bf4ea 100644 --- a/shared_directory_test.go +++ b/shared_directory_test.go @@ -139,6 +139,165 @@ func TestSingleDirectoryShare(t *testing.T) { } } +func TestDirectorySharingDevices(t *testing.T) { + if vz.Available(12) { + t.Skip("VirtioFileSystemDevice is supported from macOS 12") + } + + bootLoader, err := vz.NewLinuxBootLoader( + "./testdata/Image", + vz.WithCommandLine("console=hvc0"), + ) + if err != nil { + t.Fatalf("failed to create boot loader: %v", err) + } + + config, err := vz.NewVirtualMachineConfiguration(bootLoader, 1, 256*1024*1024) + if err != nil { + t.Fatalf("failed to create virtual machine configuration: %v", err) + } + + sharedDirectory, err := vz.NewSharedDirectory(t.TempDir(), false) + if err != nil { + t.Fatal(err) + } + single, err := vz.NewSingleDirectoryShare(sharedDirectory) + if err != nil { + t.Fatal(err) + } + fsConfig, err := vz.NewVirtioFileSystemDeviceConfiguration("share") + if err != nil { + t.Fatal(err) + } + fsConfig.SetDirectoryShare(single) + config.SetDirectorySharingDevicesVirtualMachineConfiguration( + []vz.DirectorySharingDeviceConfiguration{fsConfig}, + ) + + vm, err := vz.NewVirtualMachine(config) + if err != nil { + t.Fatalf("failed to create virtual machine: %v", err) + } + + devices := vm.DirectorySharingDevices() + if len(devices) != 1 { + t.Fatalf("expected 1 directory sharing device, got %d", len(devices)) + } + if devices[0] == nil { + t.Fatal("directory sharing device should not be nil") + } +} + +func TestVirtioFileSystemDeviceSetShare(t *testing.T) { + if vz.Available(12) { + t.Skip("VirtioFileSystemDevice is supported from macOS 12") + } + + const tag = "swap" + + // Initial share: a single directory exposing fileA. + dirA := t.TempDir() + fileA := "a.txt" + if f, err := os.Create(filepath.Join(dirA, fileA)); err != nil { + t.Fatal(err) + } else { + f.Close() + } + sharedA, err := vz.NewSharedDirectory(dirA, false) + if err != nil { + t.Fatal(err) + } + single, err := vz.NewSingleDirectoryShare(sharedA) + if err != nil { + t.Fatal(err) + } + fsConfig, err := vz.NewVirtioFileSystemDeviceConfiguration(tag) + if err != nil { + t.Fatal(err) + } + fsConfig.SetDirectoryShare(single) + + container := newVirtualizationMachine(t, + func(vmc *vz.VirtualMachineConfiguration) error { + vmc.SetDirectorySharingDevicesVirtualMachineConfiguration( + []vz.DirectorySharingDeviceConfiguration{fsConfig}, + ) + return nil + }, + ) + t.Cleanup(func() { + if err := container.Shutdown(); err != nil { + log.Println(err) + } + }) + + run := func(cmd string, wantErr bool) { + t.Helper() + session := container.NewSession(t) + defer session.Close() + var buf bytes.Buffer + session.Stderr = &buf + err := session.Run(cmd) + switch { + case err != nil && !wantErr: + t.Fatalf("failed to run command %q: %v\nstderr: %q", cmd, err, buf) + case err == nil && wantErr: + t.Fatalf("expected command %q to fail but it succeeded", cmd) + } + } + + devices := container.DirectorySharingDevices() + if len(devices) != 1 { + t.Fatalf("expected 1 directory sharing device, got %d", len(devices)) + } + device := devices[0] + + // The running device reports the single share it was configured with. + if got := device.Share(); got == nil { + t.Fatal("expected a share on the running device, got nil") + } else if _, ok := got.(*vz.SingleDirectoryShare); !ok { + t.Fatalf("expected *vz.SingleDirectoryShare, got %T", got) + } + + // The guest sees fileA through the initial share. + run("mkdir -p /mnt/shared", false) + run(fmt.Sprintf("mount -t virtiofs %s /mnt/shared", tag), false) + run("ls /mnt/shared/"+fileA, false) + + // Swap to a multiple share exposing a different directory under "sub". + dirB := t.TempDir() + fileB := "b.txt" + if f, err := os.Create(filepath.Join(dirB, fileB)); err != nil { + t.Fatal(err) + } else { + f.Close() + } + sharedB, err := vz.NewSharedDirectory(dirB, false) + if err != nil { + t.Fatal(err) + } + multiple, err := vz.NewMultipleDirectoryShare(map[string]*vz.SharedDirectory{ + "sub": sharedB, + }) + if err != nil { + t.Fatal(err) + } + device.SetShare(multiple) + + // The running device now reports the swapped-in multiple share. + if got := device.Share(); got == nil { + t.Fatal("expected a share after swap, got nil") + } else if _, ok := got.(*vz.MultipleDirectoryShare); !ok { + t.Fatalf("expected *vz.MultipleDirectoryShare after swap, got %T", got) + } + + // After remounting, the guest sees the swapped-in content and no longer fileA. + run("umount /mnt/shared", false) + run(fmt.Sprintf("mount -t virtiofs %s /mnt/shared", tag), false) + run("ls /mnt/shared/sub/"+fileB, false) + run("ls /mnt/shared/"+fileA, true) +} + func TestMultipleDirectoryShare(t *testing.T) { if vz.Available(12) { t.Skip("MultipleDirectoryShare is supported from macOS 12") diff --git a/virtualization_12.h b/virtualization_12.h index dd679a7f..27efb1ce 100644 --- a/virtualization_12.h +++ b/virtualization_12.h @@ -33,6 +33,11 @@ void *newVZMultipleDirectoryShare(void *sharedDirectories); void *newVZVirtioFileSystemDeviceConfiguration(const char *tag, void **error); void setVZVirtioFileSystemDeviceConfigurationShare(void *config, void *share); +void *VZVirtualMachine_directorySharingDevices(void *machine); +void setShareVZVirtioFileSystemDevice(void *device, void *share, void *queue); +void *getShareVZVirtioFileSystemDevice(void *device, void *queue); +bool isMultipleDirectoryShare(void *share); + void setDirectorySharingDevicesVZVirtualMachineConfiguration(void *config, void *directorySharingDevices); void setPlatformVZVirtualMachineConfiguration(void *config, void *platform); diff --git a/virtualization_12.m b/virtualization_12.m index 1ab6b524..cbba5025 100644 --- a/virtualization_12.m +++ b/virtualization_12.m @@ -59,6 +59,74 @@ void setDirectorySharingDevicesVZVirtualMachineConfiguration(void *config, void RAISE_UNSUPPORTED_MACOS_EXCEPTION(); } +/*! + @abstract Return the list of directory sharing devices configured on this virtual machine. Return an empty array if none is configured. + @see VZDirectorySharingDevice + @see VZVirtualMachineConfiguration + */ +void *VZVirtualMachine_directorySharingDevices(void *machine) +{ + if (@available(macOS 12, *)) { + return [(VZVirtualMachine *)machine directorySharingDevices]; // NSArray + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Swap the directory share on a running VZVirtioFileSystemDevice. + @discussion + VZVirtioFileSystemDevice.share is get/set on the runtime device (macOS 12+), so the + host can change which directories a guest sees without recreating the VM. The mutation + must run on the VM's serial dispatch queue, like every other VZVirtualMachine interaction. + */ +void setShareVZVirtioFileSystemDevice(void *device, void *share, void *queue) +{ + if (@available(macOS 12, *)) { + dispatch_sync((dispatch_queue_t)queue, ^{ + [(VZVirtioFileSystemDevice *)device setShare:(VZDirectoryShare *)share]; + }); + return; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Return the directory share currently set on a running VZVirtioFileSystemDevice, or NULL if none is set. + @discussion + Reading happens on the VM's serial dispatch queue like every other VZVirtualMachine + interaction. The returned share is retained for the Go caller, which releases it via + its finalizer. + */ +void *getShareVZVirtioFileSystemDevice(void *device, void *queue) +{ + if (@available(macOS 12, *)) { + __block VZDirectoryShare *share; + dispatch_sync((dispatch_queue_t)queue, ^{ + share = [(VZVirtioFileSystemDevice *)device share]; + }); + return [share retain]; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + +/*! + @abstract Report whether a VZDirectoryShare is a VZMultipleDirectoryShare. + @discussion + Lets the Go side rebuild the right concrete share type for a share read back from a + running device. A single directory share returns false. + */ +bool isMultipleDirectoryShare(void *share) +{ + if (@available(macOS 12, *)) { + return [(NSObject *)share isKindOfClass:[VZMultipleDirectoryShare class]]; + } + + RAISE_UNSUPPORTED_MACOS_EXCEPTION(); +} + /*! @abstract The hardware platform to use. @discussion