Skip to content

Commit

Permalink
Add support for GUI, virtio-input and virtio-gpu devices
Browse files Browse the repository at this point in the history
Adds command line support for the user to add virtio-gpu and
virtio-input devices which also ties into having a good experience with
the newly added gui compatability.

Adds command line support for the user to start a graphical application
window when their virtual machine starts through the `--gui` flag
(assuming the user added a virtio-gpu device as well).

Adds some mutual exclusion during creation of the
virtual machine configuration. Without calling `runtime.LockOSThread()`,
starting the virtual machine subsequently starting a graphic application
window would result in a SIGILL error. Alongside calling
`runtime.LockOSThread()`, parallelized the running of the graphic
application window and checking the status of the virtual machine.
Otherwise, the two wouldn't be able to run simultaneously.

These new features can be testing using a command similar to this:

```bash
$ ./out/vfkit --cpus 2 --memory 2048 \
--bootloader efi,variable-store=/Users/jakecorrenti/efi-variable-store,create \
--device virtio-serial,stdio \
--device virtio-fs,sharedDir=/Users/jakecorrenti,mountTag=dir0 \
--device virtio-blk,path=/Users/jakecorrenti/Downloads/Fedora-Server-dvd-x86_64-38-1.6.iso \
--device virtio-net,nat,mac=72:20:43:d4:38:62 --device virtio-input,keyboard --device virtio-input,pointing --device virtio-gpu,display --gui
```

Signed-off-by: Jake Correnti <jakecorrenti+github@proton.me>
  • Loading branch information
jakecorrenti committed Jun 9, 2023
1 parent 3d57f09 commit ba98ea8
Show file tree
Hide file tree
Showing 7 changed files with 369 additions and 10 deletions.
50 changes: 40 additions & 10 deletions cmd/vfkit/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"os"
"os/signal"
"runtime"
"syscall"
"time"

Expand Down Expand Up @@ -113,10 +114,22 @@ func waitForVMState(vm *vz.VirtualMachine, state vz.VirtualMachineState) error {
}

func runVFKit(vmConfig *config.VirtualMachine, opts *cmdline.Options) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
vzVMConfig, err := vf.ToVzVirtualMachineConfig(vmConfig)
if err != nil {
return err
}

if opts.UseGUI {
for _, gpuDev := range vmConfig.VirtioGPUDevices() {
if gpuDev.Device == config.VirtioGPUDisplayDevice {
gpuDev.UsesGUI = true
break
}
}
}

vm, err := vz.NewVirtualMachine(vzVMConfig)
if err != nil {
return err
Expand Down Expand Up @@ -168,18 +181,35 @@ func runVirtualMachine(vmConfig *config.VirtualMachine, vm *vz.VirtualMachine) e
}

log.Infof("waiting for VM to stop")
for {
err := waitForVMState(vm, vz.VirtualMachineStateStopped)
if err == nil {
log.Infof("VM is stopped")
break

errCh := make(chan error, 1)
go func() {
for {
err := waitForVMState(vm, vz.VirtualMachineStateStopped)
if err == nil {
log.Infof("VM is stopped")
errCh <- nil
return
}
if !errors.Is(err, errVMStateTimeout) {
errCh <- fmt.Errorf("virtualization error: %v", err)
return
}
// errVMStateTimeout -> keep looping
}
if !errors.Is(err, errVMStateTimeout) {
log.Infof("virtualization error: %v", err)
return err
}()

for _, gpuDev := range vmConfig.VirtioGPUDevices() {
if gpuDev.UsesGUI {
runtime.LockOSThread()
err := vm.StartGraphicApplication(float64(gpuDev.Height), float64(gpuDev.Width))
runtime.UnlockOSThread()
if err != nil {
return err
}
break
}
// errVMStateTimeout -> keep looping
}

return nil
return <-errCh
}
45 changes: 45 additions & 0 deletions doc/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,34 @@ The share can be mounted in the guest with `mount -t virtiofs vfkitTag /mnt`, wi
#### Example
`--device virtio-fs,sharedDir=/Users/virtuser/vfkit/,mountTag=vfkit-share`

### GPU

#### Description

The `--device virtio-gpu` option allows the user to add graphical devices to the virtual machine. A graphical device may include a display which can be used to start a graphical application window.
This feature currently only supports the `display` option.

#### Arguments
- `height`: the vertical resolution of the graphical device's resolution. Defaults to 800
- `width`: the horizontal resolution of the graphical device's resolution. Defaults to 600

#### Example
`--device virtio-gpu,display,height=1920,width=1080`

### Input

#### Description

The `--device virtio-input` option allows the user to add an input device to the virtual machine. This currently supports `pointing` and `keyboard` devices.

#### Arguments

None

#### Example

`--device virtio-input,pointing`


## Restful Service

Expand Down Expand Up @@ -242,3 +270,20 @@ Get description of the virtual machine

GET `/vm/inspect`
Response: { "cpus": uint, "memory": uint64, "devices": []config.VirtIODevice }

## Enabling a Graphical User Interface

### Add a virtio-gpu device

In order to successfully start a graphical application window, a virtio-gpu device must be added to the virtual machine.

### Pass the `--gui` flag

In order to tell vfkit that you want to start a graphical application window, you need to pass the `--gui` flag in your command.

### Usage

Proper use of this flag may look similar to the following section of a command:
```bash
--device virtio-input,keyboard --device virtio-input,pointing --device virtio-gpu,display,height=1920,width=1000 --gui
```
3 changes: 3 additions & 0 deletions pkg/cmdline/cmdline.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Options struct {
RestfulURI string

LogLevel string

UseGUI bool
}

const DefaultRestfulURI = "none://"
Expand All @@ -31,6 +33,7 @@ func AddFlags(cmd *cobra.Command, opts *Options) {
cmd.Flags().StringVarP(&opts.InitrdPath, "initrd", "i", "", "path to the virtual machine initrd")

cmd.Flags().VarP(&opts.Bootloader, "bootloader", "b", "bootloader configuration")
cmd.Flags().BoolVar(&opts.UseGUI, "gui", false, "display the contents of the virtual machine onto a graphical user interface")

cmd.MarkFlagsMutuallyExclusive("kernel", "bootloader")
cmd.MarkFlagsMutuallyExclusive("initrd", "bootloader")
Expand Down
11 changes: 11 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,17 @@ func (vm *VirtualMachine) AddDevicesFromCmdLine(cmdlineOpts []string) error {
return nil
}

func (vm *VirtualMachine) VirtioGPUDevices() []*VirtioGPU {
gpuDevs := []*VirtioGPU{}
for _, dev := range vm.Devices {
if gpuDev, isVirtioGPU := dev.(*VirtioGPU); isVirtioGPU {
gpuDevs = append(gpuDevs, gpuDev)
}
}

return gpuDevs
}

func (vm *VirtualMachine) VirtioVsockDevices() []*VirtioVsock {
vsockDevs := []*VirtioVsock{}
for _, dev := range vm.Devices {
Expand Down
153 changes: 153 additions & 0 deletions pkg/config/virtio.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,41 @@ import (
// The VirtioDevice interface is an interface which is implemented by all virtio devices.
type VirtioDevice VMComponent

const (
// Possible values for VirtioInput.InputType
VirtioInputPointingDevice = "pointing"
VirtioInputKeyboardDevice = "keyboard"

// Possible values for VirtioGPU.Device
VirtioGPUDisplayDevice = "display"

// Options for VirtioGPUResolution
VirtioGPUResolutionHeight = "height"
VirtioGPUResolutionWidth = "width"

// Default VirtioGPU Resolution
defaultVirtioGPUResolutionHeight = 800
defaultVirtioGPUResolutionWidth = 600
)

// VirtioInput configures an input device, such as a keyboard or pointing device
// (mouse) that the virtual machine can use
type VirtioInput struct {
InputType string `json:"inputType"` // currently supports "pointing" and "keyboard" input types
}

type VirtioGPUResolution struct {
Height int `json:"height"`
Width int `json:"width"`
}

// VirtioGPU configures a GPU device, such as the host computer's display
type VirtioGPU struct {
Device string `json:"device"` // currently supports "display" as the device
UsesGUI bool `json:"usesGUI"`
VirtioGPUResolution
}

// VirtioVsock configures of a virtio-vsock device allowing 2-way communication
// between the host and the virtual machine type
type VirtioVsock struct {
Expand Down Expand Up @@ -114,6 +149,10 @@ func deviceFromCmdLine(deviceOpts string) (VirtioDevice, error) {
dev = &VirtioVsock{}
case "usb-mass-storage":
dev = usbMassStorageNewEmpty()
case "virtio-input":
dev = &VirtioInput{}
case "virtio-gpu":
dev = &VirtioGPU{}
default:
return nil, fmt.Errorf("unknown device type: %s", opts[0])
}
Expand Down Expand Up @@ -181,6 +220,120 @@ func (dev *VirtioSerial) FromOptions(options []option) error {
return dev.validate()
}

// VirtioInputNew creates a new input device for the virtual machine.
// The inputType parameter is the type of virtio-input device that will be added
// to the machine.
func VirtioInputNew(inputType string) (VirtioDevice, error) {
dev := &VirtioInput{
InputType: inputType,
}
if err := dev.validate(); err != nil {
return nil, err
}

return dev, nil
}

func (dev *VirtioInput) validate() error {
if dev.InputType != VirtioInputPointingDevice && dev.InputType != VirtioInputKeyboardDevice {
return fmt.Errorf("Unknown option for virtio-input devices: %s", dev.InputType)
}

return nil
}

func (dev *VirtioInput) ToCmdLine() ([]string, error) {
if err := dev.validate(); err != nil {
return nil, err
}

return []string{"--device", fmt.Sprintf("virtio-input,%s", dev.InputType)}, nil
}

func (dev *VirtioInput) FromOptions(options []option) error {
for _, option := range options {
switch option.key {
case VirtioInputPointingDevice, VirtioInputKeyboardDevice:
if option.value != "" {
return fmt.Errorf(fmt.Sprintf("Unexpected value for virtio-input %s option: %s", option.key, option.value))
}
dev.InputType = option.key
default:
return fmt.Errorf("Unknown option for virtio-input devices: %s", option.key)
}
}
return dev.validate()
}

// VirtioGPUNew creates a new gpu device for the virtual machine.
// The usesGUI parameter determines whether a graphical application window will
// be displayed
func VirtioGPUNew() (VirtioDevice, error) {
return &VirtioGPU{
Device: VirtioGPUDisplayDevice,
UsesGUI: false,
VirtioGPUResolution: VirtioGPUResolution{
Height: defaultVirtioGPUResolutionHeight,
Width: defaultVirtioGPUResolutionWidth,
},
}, nil
}

func (dev *VirtioGPU) validate() error {
if dev.Device != VirtioGPUDisplayDevice {
return fmt.Errorf("Unknown option for virtio-gpu devices: %s", dev.Device)
}

if dev.Height < 1 || dev.Width < 1 {
return fmt.Errorf("Invalid dimensions for virtio-gpu device resolution: %dx%d", dev.Height, dev.Width)
}

return nil
}

func (dev *VirtioGPU) ToCmdLine() ([]string, error) {
if err := dev.validate(); err != nil {
return nil, err
}

return []string{"--device", fmt.Sprintf("virtio-gpu,%s,height=%d,width=%d", dev.Device, dev.Height, dev.Width)}, nil
}

func (dev *VirtioGPU) FromOptions(options []option) error {
for _, option := range options {
switch option.key {
case VirtioGPUDisplayDevice:
if option.value != "" {
return fmt.Errorf(fmt.Sprintf("Unexpected value for virtio-gpu %s option: %s", option.key, option.value))
}
dev.Device = option.key
case VirtioGPUResolutionHeight:
height, err := strconv.Atoi(option.value)
if err != nil || height < 1 {
return fmt.Errorf(fmt.Sprintf("Invalid value for virtio-gpu %s: %s", option.key, option.value))
}

dev.Height = height
case VirtioGPUResolutionWidth:
width, err := strconv.Atoi(option.value)
if err != nil || width < 1 {
return fmt.Errorf(fmt.Sprintf("Invalid value for virtio-gpu %s: %s", option.key, option.value))
}

dev.Width = width
default:
return fmt.Errorf("Unknown option for virtio-gpu devices: %s", option.key)
}
}

if dev.Width == 0 && dev.Height == 0 {
dev.Width = defaultVirtioGPUResolutionWidth
dev.Height = defaultVirtioGPUResolutionHeight
}

return dev.validate()
}

// VirtioNetNew creates a new network device for the virtual machine. It will
// use macAddress as its MAC address.
func VirtioNetNew(macAddress string) (*VirtioNet, error) {
Expand Down
39 changes: 39 additions & 0 deletions pkg/config/virtio_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,45 @@ var virtioDevTests = map[string]virtioDevTest{
},
expectedCmdLine: []string{"--device", "usb-mass-storage,path=/foo/bar"},
},
"NewVirtioInputWithPointingDevice": {
newDev: func() (VirtioDevice, error) { return VirtioInputNew("pointing") },
expectedDev: &VirtioInput{
InputType: "pointing",
},
expectedCmdLine: []string{"--device", "virtio-input,pointing"},
},
"NewVirtioInputWithKeyboardDevice": {
newDev: func() (VirtioDevice, error) { return VirtioInputNew("keyboard") },
expectedDev: &VirtioInput{
InputType: "keyboard",
},
expectedCmdLine: []string{"--device", "virtio-input,keyboard"},
},
"NewVirtioGPUDevice": {
newDev: VirtioGPUNew,
expectedDev: &VirtioGPU{
"display",
false,
VirtioGPUResolution{800, 600},
},
expectedCmdLine: []string{"--device", "virtio-gpu,display,height=800,width=600"},
},
"NewVirtioGPUDeviceWithDimensions": {
newDev: func() (VirtioDevice, error) {
dev, err := VirtioGPUNew()
if err != nil {
return nil, err
}
dev.(*VirtioGPU).VirtioGPUResolution = VirtioGPUResolution{1920, 1080}
return dev, nil
},
expectedDev: &VirtioGPU{
"display",
false,
VirtioGPUResolution{1920, 1080},
},
expectedCmdLine: []string{"--device", "virtio-gpu,display,height=1920,width=1080"},
},
}

func testVirtioDev(t *testing.T, test *virtioDevTest) {
Expand Down
Loading

0 comments on commit ba98ea8

Please sign in to comment.