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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ lima.REJECTED.yaml
default-template.yaml
schema-limayaml.json
.config
.DS_Store
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gitignore should be in your own home directory

77 changes: 77 additions & 0 deletions pkg/driver/vz/spice_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

//go:build darwin

package vz

import (
"github.com/Code-Hex/vz/v3"
"github.com/sirupsen/logrus"

"github.com/lima-vm/lima/v2/pkg/limatype"
)

// attachSpiceAgent configures SPICE agent for clipboard sharing.
// This enables bidirectional clipboard sharing between host and guest.
// SPICE agent requires a display to be useful.
func attachSpiceAgent(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {
// SPICE agent only makes sense with a display
if inst.Config.Video.Display == nil || *inst.Config.Video.Display == "none" {
return nil
}

// Check clipboard configuration
// Default: enabled when display is present
// Respect explicit user configuration (true or false)
if inst.Config.Video.Clipboard != nil && !*inst.Config.Video.Clipboard {
logrus.Debug("Clipboard sharing explicitly disabled in configuration")
return nil
}

// Get the SPICE agent port name
portName, err := vz.SpiceAgentPortAttachmentName()
if err != nil {
logrus.Warnf("Failed to get SPICE agent port name: %v", err)
return nil // Not fatal, clipboard just won't work
}

// Create SPICE agent port attachment
spiceAgent, err := vz.NewSpiceAgentPortAttachment()
if err != nil {
logrus.Warnf("Failed to create SPICE agent: %v", err)
return nil // Not fatal, clipboard just won't work
}

// Enable clipboard sharing
spiceAgent.SetSharesClipboard(true)

// Create virtio console device if not already configured
consoleDevice, err := vz.NewVirtioConsoleDeviceConfiguration()
if err != nil {
logrus.Warnf("Failed to create console device for SPICE: %v", err)
return nil
}

// Create console port configuration with SPICE agent
portConfig, err := vz.NewVirtioConsolePortConfiguration(
vz.WithVirtioConsolePortConfigurationName(portName),
vz.WithVirtioConsolePortConfigurationAttachment(spiceAgent),
)
if err != nil {
logrus.Warnf("Failed to create SPICE agent port configuration: %v", err)
return nil
}

// Attach the port to the console device
// Port 0 is typically the first console port
consoleDevice.SetVirtioConsolePortConfiguration(0, portConfig)

// Set the console device in the VM configuration
vmConfig.SetConsoleDevicesVirtualMachineConfiguration([]vz.ConsoleDeviceConfiguration{
consoleDevice,
})

logrus.Info("SPICE agent configured for clipboard sharing")
return nil
}
66 changes: 58 additions & 8 deletions pkg/driver/vz/vm_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ func createVM(ctx context.Context, inst *limatype.Instance) (*vz.VirtualMachine,
return nil, err
}

// Attach SPICE agent for clipboard sharing (requires macOS 13+)
if err = attachSpiceAgent(inst, vmConfig); err != nil {
return nil, err
}

if err = attachFolderMounts(inst, vmConfig); err != nil {
return nil, err
}
Expand Down Expand Up @@ -551,14 +556,28 @@ func attachDisks(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Virt
func attachDisplay(inst *limatype.Instance, vmConfig *vz.VirtualMachineConfiguration) error {
switch *inst.Config.Video.Display {
case "vz", "default":
width := 1920
height := 1200

if inst.Config.Video.VZ.Width != nil {
width = *inst.Config.Video.VZ.Width
}
if inst.Config.Video.VZ.Height != nil {
height = *inst.Config.Video.VZ.Height
}
if inst.Config.Video.VZ.PixelsPerInch != nil {
logrus.Warnf("video.vz.pixelsPerInch is not yet supported by Apple Virtualization.framework")
}

graphicsDeviceConfiguration, err := vz.NewVirtioGraphicsDeviceConfiguration()
if err != nil {
return err
}
scanoutConfiguration, err := vz.NewVirtioGraphicsScanoutConfiguration(1920, 1200)
scanoutConfiguration, err := vz.NewVirtioGraphicsScanoutConfiguration(int64(width), int64(height))
if err != nil {
return err
}

graphicsDeviceConfiguration.SetScanouts(scanoutConfiguration)

vmConfig.SetGraphicsDevicesVirtualMachineConfiguration([]vz.GraphicsDeviceConfiguration{
Expand Down Expand Up @@ -626,18 +645,49 @@ func attachFolderMounts(inst *limatype.Instance, vmConfig *vz.VirtualMachineConf
func attachAudio(inst *limatype.Instance, config *vz.VirtualMachineConfiguration) error {
switch *inst.Config.Audio.Device {
case "vz", "default":
outputStream, err := vz.NewVirtioSoundDeviceHostOutputStreamConfiguration()
if err != nil {
return err
// Check what's enabled (default: output only, no input/microphone)
inputEnabled := false
outputEnabled := true

if inst.Config.Audio.VZ.InputEnabled != nil {
inputEnabled = *inst.Config.Audio.VZ.InputEnabled
}
if inst.Config.Audio.VZ.OutputEnabled != nil {
outputEnabled = *inst.Config.Audio.VZ.OutputEnabled
}

soundDeviceConfiguration, err := vz.NewVirtioSoundDeviceConfiguration()
if err != nil {
return err
}
soundDeviceConfiguration.SetStreams(outputStream)
config.SetAudioDevicesVirtualMachineConfiguration([]vz.AudioDeviceConfiguration{
soundDeviceConfiguration,
})

var streams []vz.VirtioSoundDeviceStreamConfiguration

if outputEnabled {
outputStream, err := vz.NewVirtioSoundDeviceHostOutputStreamConfiguration()
if err != nil {
return err
}
streams = append(streams, outputStream)
logrus.Debug("VZ audio output enabled")
}

if inputEnabled {
inputStream, err := vz.NewVirtioSoundDeviceHostInputStreamConfiguration()
if err != nil {
return err
}
streams = append(streams, inputStream)
logrus.Info("VZ audio input (microphone) enabled")
}

if len(streams) > 0 {
soundDeviceConfiguration.SetStreams(streams...)
config.SetAudioDevicesVirtualMachineConfiguration([]vz.AudioDeviceConfiguration{
soundDeviceConfiguration,
})
}

return nil
case "", "none":
return nil
Expand Down
86 changes: 61 additions & 25 deletions pkg/driver/vz/vz_driver_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,40 +352,76 @@ func (l *LimaVzDriver) canRunGUI() bool {
}

func (l *LimaVzDriver) RunGUI() error {
if l.canRunGUI() {
return l.machine.StartGraphicApplication(1920, 1200)
if !l.canRunGUI() {
return fmt.Errorf("RunGUI is not supported for the given driver '%s' and display '%s'", "vz", *l.Instance.Config.Video.Display)
}
return fmt.Errorf("RunGUI is not supported for the given driver '%s' and display '%s'", "vz", *l.Instance.Config.Video.Display)

if l.machine == nil {
return fmt.Errorf("cannot show GUI: VM is not running (machine not initialized). Start the VM first with 'limactl start %s'", l.Instance.Name)
}

// Default to a reasonable window size if not specified
width := 1440
height := 900

if l.Instance.Config.Video.VZ.Width != nil {
width = *l.Instance.Config.Video.VZ.Width
}
if l.Instance.Config.Video.VZ.Height != nil {
height = *l.Instance.Config.Video.VZ.Height
}

title := fmt.Sprintf("Lima: %s", l.Instance.Name)

return l.machine.StartGraphicApplication(
float64(width),
float64(height),
vz.WithWindowTitle(title),
vz.WithController(true),
)
}

func (l *LimaVzDriver) Stop(_ context.Context) error {
logrus.Info("Shutting down VZ")
canStop := l.machine.CanRequestStop()
logrus.Info("Shutting down VZ with graceful stop request")

if canStop {
_, err := l.machine.RequestStop()
if err != nil {
return err
}
if !l.machine.CanRequestStop() {
logrus.Warn("VZ: CanRequestStop is not supported, forcing immediate stop")
return l.machine.Stop()
}

timeout := time.After(5 * time.Second)
ticker := time.NewTicker(500 * time.Millisecond)
for {
select {
case <-timeout:
return errors.New("vz timeout while waiting for stop status")
case <-ticker.C:
l.machine.mu.Lock()
stopped := l.machine.stopped
l.machine.mu.Unlock()
if stopped {
return nil
}
// Request graceful shutdown (similar to ACPI power button)
result, err := l.machine.RequestStop()
if err != nil {
logrus.WithError(err).Warn("Failed to send stop request, forcing immediate stop")
return l.machine.Stop()
}

if !result {
logrus.Warn("VZ: RequestStop returned false, forcing immediate stop")
return l.machine.Stop()
}

// Wait for graceful shutdown with timeout (30 seconds like QEMU)
timeout := time.After(30 * time.Second)
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-timeout:
logrus.Warn("VZ timeout while waiting for graceful shutdown, forcing stop")
return l.machine.Stop()

case <-ticker.C:
l.machine.mu.Lock()
stopped := l.machine.stopped
l.machine.mu.Unlock()
if stopped {
logrus.Info("VZ has shut down gracefully")
return nil
}
}
}

return errors.New("vz: CanRequestStop is not supported")
}

func (l *LimaVzDriver) GuestAgentConn(_ context.Context) (net.Conn, string, error) {
Expand Down
28 changes: 26 additions & 2 deletions pkg/limatype/lima_yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,19 +212,43 @@ type Firmware struct {
Images []FileWithVMType `yaml:"images,omitempty" json:"images,omitempty"`
}

type VZAudioOptions struct {
// InputEnabled enables audio input (microphone) from host to guest (default: false)
InputEnabled *bool `yaml:"inputEnabled,omitempty" json:"inputEnabled,omitempty" jsonschema:"nullable"`
// OutputEnabled enables audio output (speakers) from guest to host (default: true)
OutputEnabled *bool `yaml:"outputEnabled,omitempty" json:"outputEnabled,omitempty" jsonschema:"nullable"`
}

type Audio struct {
// Device is a QEMU audiodev string
Device *string `yaml:"device,omitempty" json:"device,omitempty" jsonschema:"nullable"`
// Device is a QEMU audiodev string, or "vz" for VZ driver
Device *string `yaml:"device,omitempty" json:"device,omitempty" jsonschema:"nullable"`
VZ VZAudioOptions `yaml:"vz,omitempty" json:"vz,omitempty"`
}

type VNCOptions struct {
Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"`
}

type VZOptions struct {
// Width is the display width in pixels (default: 1920)
Width *int `yaml:"width,omitempty" json:"width,omitempty" jsonschema:"nullable"`
// Height is the display height in pixels (default: 1200)
Height *int `yaml:"height,omitempty" json:"height,omitempty" jsonschema:"nullable"`
// PixelsPerInch configures display density (reserved for future use, not yet supported by Apple Virtualization.framework)
// Intended for Retina/HiDPI support: standard ~80-100, Retina 144+
PixelsPerInch *int `yaml:"pixelsPerInch,omitempty" json:"pixelsPerInch,omitempty" jsonschema:"nullable"`
}

type Video struct {
// Display is a QEMU display string
Display *string `yaml:"display,omitempty" json:"display,omitempty" jsonschema:"nullable"`
VNC VNCOptions `yaml:"vnc,omitempty" json:"vnc,omitempty"`
VZ VZOptions `yaml:"vz,omitempty" json:"vz,omitempty"`
// Clipboard enables clipboard sharing between host and guest (default: true when display is enabled)
// For VZ: Uses SPICE agent protocol via virtio console. Requires display and spice-vdagent in guest.
// For QEMU: Depends on the display backend capabilities.
// Note: Clipboard requires a graphical display to be configured.
Clipboard *bool `yaml:"clipboard,omitempty" json:"clipboard,omitempty" jsonschema:"nullable"`
}

type ProvisionMode = string
Expand Down
Loading
Loading