Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions shared_directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package vz
import "C"
import (
"os"
"unsafe"

"github.com/Code-Hex/vz/v3/internal/objc"
)
Expand Down Expand Up @@ -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
}
159 changes: 159 additions & 0 deletions shared_directory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
5 changes: 5 additions & 0 deletions virtualization_12.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
68 changes: 68 additions & 0 deletions virtualization_12.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<VZDirectorySharingDevice *>
}

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
Expand Down