From 5698ac56a6f53f65f2aed5c664c81ca5975bc6e0 Mon Sep 17 00:00:00 2001 From: Justin Alvarez Date: Mon, 7 Aug 2023 20:05:05 -0400 Subject: [PATCH] feat: add WSL2 driver Signed-off-by: Justin Alvarez --- Makefile | 11 + cmd/lima-guestagent/daemon_linux.go | 32 ++- cmd/lima-guestagent/install_systemd_linux.go | 23 +- .../lima-guestagent.TEMPLATE.service | 2 +- cmd/limactl/delete.go | 1 + cmd/limactl/hostagent.go | 1 + cmd/limactl/shell.go | 2 +- docs/experimental.md | 1 + docs/mount.md | 19 ++ docs/vmtype.md | 33 +++ examples/experimental/wsl2.yaml | 17 ++ go.mod | 9 +- go.sum | 6 +- .../cidata.TEMPLATE.d/boot/02-wsl2-setup.sh | 17 ++ .../boot/25-guestagent-base.sh | 6 +- pkg/cidata/cidata.TEMPLATE.d/lima.env | 2 + pkg/cidata/cidata.go | 45 +++- pkg/cidata/template.go | 2 + pkg/downloader/downloader.go | 2 +- pkg/driverutil/driverutil.go | 4 + pkg/driverutil/instance.go | 4 + pkg/executil/command.go | 51 ++++ pkg/guestagent/api/client/client.go | 38 ++- pkg/guestagent/api/client/client_others.go | 16 ++ pkg/guestagent/api/client/client_windows.go | 19 ++ pkg/guestagent/guestagent_linux.go | 26 +- pkg/hostagent/hostagent.go | 88 +++++-- pkg/hostagent/port.go | 22 +- pkg/hostagent/port_darwin.go | 4 + pkg/hostagent/port_others.go | 8 +- pkg/hostagent/port_windows.go | 19 ++ pkg/hostagent/requirements.go | 31 ++- pkg/httpclientutil/httpclientutil.go | 19 -- pkg/httpclientutil/httpclientutil_others.go | 43 ++++ pkg/httpclientutil/httpclientutil_windows.go | 65 +++++ pkg/ioutilx/ioutilx.go | 35 +++ pkg/limayaml/defaults.go | 2 + pkg/limayaml/limayaml.go | 2 + pkg/limayaml/validate.go | 8 +- pkg/osutil/osutil_windows.go | 11 +- pkg/sshutil/sshutil.go | 30 ++- pkg/start/ha_cmd_opts_others.go | 10 + pkg/start/ha_cmd_opts_windows.go | 9 + pkg/start/start.go | 1 + pkg/store/filenames/filenames.go | 1 + pkg/store/instance.go | 87 ++++--- pkg/store/instance_test.go | 11 +- pkg/store/instance_unix.go | 14 ++ pkg/store/instance_windows.go | 101 ++++++++ pkg/windows/process_windows.go | 40 ++++ pkg/windows/registry_windows.go | 225 ++++++++++++++++++ pkg/windows/wsl_util_windows.go | 44 ++++ pkg/wsl2/fs.go | 35 +++ pkg/wsl2/lima-init.TEMPLATE.sh | 5 + pkg/wsl2/vm_windows.go | 142 +++++++++++ pkg/wsl2/wsl_driver_others.go | 41 ++++ pkg/wsl2/wsl_driver_windows.go | 170 +++++++++++++ 57 files changed, 1573 insertions(+), 139 deletions(-) create mode 100644 examples/experimental/wsl2.yaml create mode 100755 pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh create mode 100644 pkg/executil/command.go create mode 100644 pkg/guestagent/api/client/client_others.go create mode 100644 pkg/guestagent/api/client/client_windows.go create mode 100644 pkg/hostagent/port_windows.go create mode 100644 pkg/httpclientutil/httpclientutil_others.go create mode 100644 pkg/httpclientutil/httpclientutil_windows.go create mode 100644 pkg/start/ha_cmd_opts_others.go create mode 100644 pkg/start/ha_cmd_opts_windows.go create mode 100644 pkg/store/instance_unix.go create mode 100644 pkg/store/instance_windows.go create mode 100644 pkg/windows/process_windows.go create mode 100644 pkg/windows/registry_windows.go create mode 100644 pkg/windows/wsl_util_windows.go create mode 100644 pkg/wsl2/fs.go create mode 100644 pkg/wsl2/lima-init.TEMPLATE.sh create mode 100755 pkg/wsl2/vm_windows.go create mode 100644 pkg/wsl2/wsl_driver_others.go create mode 100644 pkg/wsl2/wsl_driver_windows.go diff --git a/Makefile b/Makefile index 253deab4e583..cb458c41b1de 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,17 @@ GO_BUILDTAGS += no_vz endif endif +ifeq ($(GOOS),windows) +WINVER_MAJOR=$(shell powershell.exe "[System.Environment]::OSVersion.Version.Major") +ifeq ($(WINVER_MAJOR),10) +WINVER_BUILD=$(shell powershell.exe "[System.Environment]::OSVersion.Version.Build") +WINVER_BUILD_HIGH_ENOUGH=$(shell powershell.exe $(WINVER_BUILD) -ge 19041) +ifeq ($(WINVER_BUILD_HIGH_ENOUGH),False) +GO_BUILDTAGS += no_wsl +endif +endif +endif + PACKAGE := github.com/lima-vm/lima VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags) diff --git a/cmd/lima-guestagent/daemon_linux.go b/cmd/lima-guestagent/daemon_linux.go index 46db9dc8287f..ff82ecff4b2a 100644 --- a/cmd/lima-guestagent/daemon_linux.go +++ b/cmd/lima-guestagent/daemon_linux.go @@ -10,6 +10,7 @@ import ( "github.com/gorilla/mux" "github.com/lima-vm/lima/pkg/guestagent" "github.com/lima-vm/lima/pkg/guestagent/api/server" + "github.com/mdlayher/vsock" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -21,6 +22,7 @@ func newDaemonCommand() *cobra.Command { RunE: daemonAction, } daemonCommand.Flags().Duration("tick", 3*time.Second, "tick for polling events") + daemonCommand.Flags().Int("vsock-port", 0, "use vsock server instead a UNIX socket") return daemonCommand } @@ -30,6 +32,10 @@ func daemonAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } + vSockPort, err := cmd.Flags().GetInt("vsock-port") + if err != nil { + return err + } if tick == 0 { return errors.New("tick must be specified") } @@ -60,13 +66,25 @@ func daemonAction(cmd *cobra.Command, _ []string) error { if err != nil { return err } - l, err := net.Listen("unix", socket) - if err != nil { - return err - } - if err := os.Chmod(socket, 0777); err != nil { - return err + + var l net.Listener + if vSockPort != 0 { + vsockL, err := vsock.Listen(uint32(vSockPort), nil) + if err != nil { + return err + } + l = vsockL + logrus.Infof("serving the guest agent on vsock port: %d", vSockPort) + } else { + socketL, err := net.Listen("unix", socket) + if err != nil { + return err + } + if err := os.Chmod(socket, 0777); err != nil { + return err + } + l = socketL + logrus.Infof("serving the guest agent on %q", socket) } - logrus.Infof("serving the guest agent on %q", socket) return srv.Serve(l) } diff --git a/cmd/lima-guestagent/install_systemd_linux.go b/cmd/lima-guestagent/install_systemd_linux.go index 60fd5b36e1a1..112c3dd351fc 100644 --- a/cmd/lima-guestagent/install_systemd_linux.go +++ b/cmd/lima-guestagent/install_systemd_linux.go @@ -3,6 +3,7 @@ package main import ( _ "embed" "errors" + "fmt" "os" "os/exec" "path/filepath" @@ -19,11 +20,16 @@ func newInstallSystemdCommand() *cobra.Command { Short: "install a systemd unit (user)", RunE: installSystemdAction, } + installSystemdCommand.Flags().Int("vsock-port", 0, "use vsock server on specified port") return installSystemdCommand } -func installSystemdAction(_ *cobra.Command, _ []string) error { - unit, err := generateSystemdUnit() +func installSystemdAction(cmd *cobra.Command, _ []string) error { + vsockPort, err := cmd.Flags().GetInt("vsock-port") + if err != nil { + return err + } + unit, err := generateSystemdUnit(vsockPort) if err != nil { return err } @@ -40,11 +46,11 @@ func installSystemdAction(_ *cobra.Command, _ []string) error { return err } logrus.Infof("Written file %q", unitPath) - argss := [][]string{ + args := [][]string{ {"daemon-reload"}, {"enable", "--now", "lima-guestagent.service"}, } - for _, args := range argss { + for _, args := range args { cmd := exec.Command("systemctl", append([]string{"--system"}, args...)...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -60,13 +66,20 @@ func installSystemdAction(_ *cobra.Command, _ []string) error { //go:embed lima-guestagent.TEMPLATE.service var systemdUnitTemplate string -func generateSystemdUnit() ([]byte, error) { +func generateSystemdUnit(vsockPort int) ([]byte, error) { selfExeAbs, err := os.Executable() if err != nil { return nil, err } + + var args []string + if vsockPort != 0 { + args = append(args, fmt.Sprintf("--vsock-port %d", vsockPort)) + } + m := map[string]string{ "Binary": selfExeAbs, + "Args": strings.Join(args, " "), } return textutil.ExecuteTemplate(systemdUnitTemplate, m) } diff --git a/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service b/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service index ddbd24b7e678..dc10e21c796d 100644 --- a/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service +++ b/cmd/lima-guestagent/lima-guestagent.TEMPLATE.service @@ -2,7 +2,7 @@ Description=lima-guestagent [Service] -ExecStart={{.Binary}} daemon +ExecStart={{.Binary}} daemon {{.Args}} Type=simple Restart=on-failure diff --git a/cmd/limactl/delete.go b/cmd/limactl/delete.go index 55dd20c28b3e..97359c6fc321 100644 --- a/cmd/limactl/delete.go +++ b/cmd/limactl/delete.go @@ -62,6 +62,7 @@ func deleteInstance(ctx context.Context, inst *store.Instance, force bool) error if err := os.RemoveAll(inst.Dir); err != nil { return fmt.Errorf("failed to remove %q: %w", inst.Dir, err) } + return nil } diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go index 78e6b615676e..e412a8da4011 100644 --- a/cmd/limactl/hostagent.go +++ b/cmd/limactl/hostagent.go @@ -98,6 +98,7 @@ func hostagentAction(cmd *cobra.Command, args []string) error { return err } l, err := net.Listen("unix", socket) + logrus.Infof("hostagent socket created at %s", socket) if err != nil { return err } diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index b508c2eef7bd..00e75be024cb 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -184,7 +184,7 @@ func shellAction(cmd *cobra.Command, args []string) error { sshArgs = append(sshArgs, []string{ "-q", "-p", strconv.Itoa(inst.SSHLocalPort), - "127.0.0.1", + inst.SSHAddress, "--", script, }...) diff --git a/docs/experimental.md b/docs/experimental.md index 812bffc3ca85..1a5f73e764e2 100644 --- a/docs/experimental.md +++ b/docs/experimental.md @@ -5,6 +5,7 @@ The following features are experimental and subject to change: - `mountType: 9p` - `mountType: virtiofs` on Linux - `vmType: vz` and relevant configurations (`mountType: virtiofs`, `rosetta`, `[]networks.vzNAT`) +- `vmType: wsl2` and relevant configurations (`mountType: wsl2`) - `arch: riscv64` - `video.display: vnc` and relevant configuration (`video.vnc.display`) - `mode: user-v2` in `networks.yml` and relevant configuration in `lima.yaml` diff --git a/docs/mount.md b/docs/mount.md index a92313b1f40c..3d2c4e014e58 100644 --- a/docs/mount.md +++ b/docs/mount.md @@ -106,3 +106,22 @@ mounts: - For macOS, the "virtiofs" mount type is supported only on macOS 13 or above with `vmType: vz` config. See also [`vmtype.md`](./vmtype.md). - For Linux, the "virtiofs" mount type requires the [Rust version of virtiofsd](https://gitlab.com/virtio-fs/virtiofsd). Using the version from QEMU (usually packaged as `qemu-virtiofsd`) will *not* work, as it requires root access to run. + +### wsl2 +> **Warning** +> "wsl2" mode is experimental + +| :zap: Requirement | Lima >= 0.18 + (Windows >= 10 Build 19041 OR Windows 11) | +| ----------------- | -------------------------------------------------------- | + +The "wsl2" mount type relies on using WSL2's navite disk sharing, where the root disk is available by default at `/mnt/$DISK_LETTER` (e.g. `/mnt/c/`). + +An example configuration: +```yaml +vmType: "wsl2" +mountType: "wsl2" +``` + +#### Caveats +- WSL2 file permissions may not work exactly as expected when accessing files that are natively on the Windows disk ([more info](https://github.com/MicrosoftDocs/WSL/blob/mattw-wsl2-explainer/WSL/file-permissions.md)) +- WSL2's disk sharing system uses a 9P protocol server, making the performance similar to [Lima's 9p](#9p) mode ([more info](https://github.com/MicrosoftDocs/WSL/blob/mattw-wsl2-explainer/WSL/wsl2-architecture.md#wsl-2-architectural-flow)) diff --git a/docs/vmtype.md b/docs/vmtype.md index 825fce20eb26..44605841057a 100644 --- a/docs/vmtype.md +++ b/docs/vmtype.md @@ -45,3 +45,36 @@ mountType: "virtiofs" kernel v6.3 and later should boot, as long as it is booted via GRUB. https://github.com/lima-vm/lima/issues/1577#issuecomment-1565625668 The issue is fixed in macOS 13.5. + +## WSL2 +> **Warning** +> "wsl2" mode is experimental + +| :zap: Requirement | Lima >= 0.14, macOS >= 13.0 | +|-------------------|-----------------------------| + +"wsl2" option makes use of native virtualization support provided by Windows' `wsl.exe` ([more info](https://learn.microsoft.com/en-us/windows/wsl/about)). + +An example configuration: +```yaml +# Example to run Fedora using vmType: wsl2 +vmType: wsl2 +images: +# Source: https://github.com/runfinch/finch-core/blob/main/Dockerfile +- location: "https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1690920103.tar.zst" + arch: "x86_64" + digest: "sha256:53f2e329b8da0f6a25e025d1f6cc262ae228402ba615ad095739b2f0ec6babc9" +mountType: wsl2 +containerd: + system: true + user: false +``` + +### Caveats +- "wsl2" option is only supported on newer versions of Windows (roughly anything since 2019) + +### Known Issues +- "wsl2" currently doesn't support many of Lima's options. See [this file](../pkg/wsl2/wsl_driver_windows.go#35) for the latest supported options. +- When running lima using "wsl2", `${LIMA_HOME}//serial.log` will not contain kernel boot logs +- WSL2 requires a `tar` formatted rootfs archive instead of a VM image +- Windows doesn't ship with ssh.exe, gzip.exe, etc. which are used by Lima at various points. The easiest way around this is to run `winget install -e --id Git.MinGit` (winget is now built in to Windows as well), and add the resulting `C:\Program Files\Git\usr\bin\` directory to your path. diff --git a/examples/experimental/wsl2.yaml b/examples/experimental/wsl2.yaml new file mode 100644 index 000000000000..653ca8fb3453 --- /dev/null +++ b/examples/experimental/wsl2.yaml @@ -0,0 +1,17 @@ +# This template requires Lima v0.18.0 or later and only works on Windows versions +# that support WSL2 (Windows 10 Build >= 19041, all Windows 11). +vmType: wsl2 + +images: +# Source: https://github.com/runfinch/finch-core/blob/main/Dockerfile +- location: "https://deps.runfinch.com/common/x86-64/finch-rootfs-production-amd64-1690920103.tar.zst" + arch: "x86_64" + digest: "sha256:53f2e329b8da0f6a25e025d1f6cc262ae228402ba615ad095739b2f0ec6babc9" + +mountType: wsl2 + +# Use system because of an error when setting up RootlessKit (see https://github.com/microsoft/WSL/issues/8842) +# There are possible workarounds, just not implemented yet. +containerd: + system: true + user: false diff --git a/go.mod b/go.mod index fe1d62891913..e68e8b50760a 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( require ( github.com/Code-Hex/go-infinity-channel v1.0.0 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/go-winio v0.6.1 github.com/VividCortex/ewma v1.2.0 // indirect github.com/a8m/envsubst v1.4.2 // indirect github.com/alecthomas/participle/v2 v2.0.0 // indirect @@ -89,7 +89,7 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mdlayher/socket v0.4.1 // indirect - github.com/mdlayher/vsock v1.2.1 // indirect + github.com/mdlayher/vsock v1.2.1 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -103,11 +103,12 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.7.0 // indirect golang.org/x/crypto v0.12.0 // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b + golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect golang.org/x/term v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/text v0.12.0 golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.9.3 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect diff --git a/go.sum b/go.sum index 160174c687e6..51457a58672e 100644 --- a/go.sum +++ b/go.sum @@ -273,14 +273,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh new file mode 100755 index 000000000000..f26fc2699231 --- /dev/null +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# This script replaces the cloud-init functionality of creating a user and setting its SSH keys +# when using a WSL2 VM. +[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0 + +# create user +sudo useradd -u "${LIMA_CIDATA_UID}" "${LIMA_CIDATA_USER}" -d /home/"${LIMA_CIDATA_USER}".linux/ +sudo mkdir /home/"${LIMA_CIDATA_USER}".linux/.ssh/ +sudo cp "${LIMA_CIDATA_MNT}"/ssh_authorized_keys /home/"${LIMA_CIDATA_USER}".linux/.ssh/authorized_keys +sudo chown "${LIMA_CIDATA_USER}" /home/"${LIMA_CIDATA_USER}".linux/.ssh/authorized_keys + +# add lima to sudoers +sudo echo "lima ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/99_lima_sudoers + +# copy some CIDATA to the hardcoded path for requirement checks (TODO: make this not hardcoded) +sudo mkdir -p /mnt/lima-cidata +sudo cp "${LIMA_CIDATA_MNT}"/meta-data /mnt/lima-cidata/meta-data \ No newline at end of file diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh index 3276ec2adeaa..379f9c4b3bf0 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh +++ b/pkg/cidata/cidata.TEMPLATE.d/boot/25-guestagent-base.sh @@ -40,5 +40,9 @@ else # Remove legacy systemd service rm -f "${LIMA_CIDATA_HOME}/.config/systemd/user/lima-guestagent.service" - sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd + if [ "$LIMA_CIDATA_VMTYPE" = "wsl2" ]; then + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd --vsock-port "${LIMA_CIDATA_VSOCK_PORT}" + else + sudo "${LIMA_CIDATA_GUEST_INSTALL_PREFIX}"/bin/lima-guestagent install-systemd + fi fi diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env index 3838d28f2c66..d1c7a2b05075 100644 --- a/pkg/cidata/cidata.TEMPLATE.d/lima.env +++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env @@ -39,3 +39,5 @@ LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION=1 {{- else}} LIMA_CIDATA_SKIP_DEFAULT_DEPENDENCY_RESOLUTION= {{- end}} +LIMA_CIDATA_VMTYPE={{ .VMType }} +LIMA_CIDATA_VSOCK_PORT={{ .VSockPort }} diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go index 6a3fdc21ed75..bd344f3ed6af 100644 --- a/pkg/cidata/cidata.go +++ b/pkg/cidata/cidata.go @@ -7,22 +7,23 @@ import ( "net" "net/url" "os" + "path" "path/filepath" "strconv" "strings" "time" - "github.com/lima-vm/lima/pkg/networks" - "github.com/docker/go-units" "github.com/lima-vm/lima/pkg/iso9660util" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/localpathutil" + "github.com/lima-vm/lima/pkg/networks" "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/sshutil" "github.com/lima-vm/lima/pkg/store/filenames" "github.com/lima-vm/lima/pkg/usrlocalsharelima" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) var netLookupIP = func(host string) []net.IP { @@ -105,7 +106,7 @@ func setupEnv(y *limayaml.LimaYAML) (map[string]string, error) { return env, nil } -func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string) error { +func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, nerdctlArchive string, vsockPort int) error { if err := limayaml.Validate(*y, false); err != nil { return err } @@ -130,6 +131,8 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort SlirpIPAddress: networks.SlirpIPAddress, RosettaEnabled: *y.Rosetta.Enabled, RosettaBinFmt: *y.Rosetta.BinFmt, + VMType: *y.VMType, + VSockPort: vsockPort, } // change instance id on every boot so network config will be processed again @@ -327,6 +330,14 @@ func GenerateISO9660(instDir, name string, y *limayaml.LimaYAML, udpDNSLocalPort }) } + if args.VMType == limayaml.WSL2 { + layout = append(layout, iso9660util.Entry{ + Path: "ssh_authorized_keys", + Reader: strings.NewReader(strings.Join(args.SSHPubKeys, "\n")), + }) + return writeCIDataDir(filepath.Join(instDir, filenames.CIDataISODir), layout) + } + return iso9660util.Write(filepath.Join(instDir, filenames.CIDataISO), "cidata", layout) } @@ -377,3 +388,31 @@ func getBootCmds(p []limayaml.Provision) []BootCmds { func diskDeviceNameFromOrder(order int) string { return fmt.Sprintf("vd%c", int('b')+order) } + +func writeCIDataDir(rootPath string, layout []iso9660util.Entry) error { + slices.SortFunc(layout, func(a, b iso9660util.Entry) int { + return strings.Compare(strings.ToLower(a.Path), strings.ToLower(b.Path)) + }) + + if err := os.RemoveAll(rootPath); err != nil { + return err + } + + for _, e := range layout { + if dir := path.Dir(e.Path); dir != "" && dir != "/" { + if err := os.MkdirAll(filepath.Join(rootPath, dir), 0700); err != nil { + return err + } + } + f, err := os.OpenFile(filepath.Join(rootPath, e.Path), os.O_CREATE|os.O_RDWR, 0700) + if err != nil { + return err + } + defer f.Close() + if _, err := io.Copy(f, e.Reader); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go index 76410f8aeee9..956c36638f34 100644 --- a/pkg/cidata/template.go +++ b/pkg/cidata/template.go @@ -79,6 +79,8 @@ type TemplateArgs struct { RosettaEnabled bool RosettaBinFmt bool SkipDefaultDependencyResolution bool + VMType string + VSockPort int } func ValidateTemplateArgs(args TemplateArgs) error { diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go index af46a91070db..39f21de715fc 100644 --- a/pkg/downloader/downloader.go +++ b/pkg/downloader/downloader.go @@ -353,7 +353,7 @@ func decompressLocal(dst, src, ext string, description string) error { if description == "" { description = filepath.Base(src) } - fmt.Printf("Decompressing %s\n", description) + logrus.Infof("Decompressing %s\n", description) } bar.Start() err = cmd.Run() diff --git a/pkg/driverutil/driverutil.go b/pkg/driverutil/driverutil.go index 1cddfd10626c..a0d12787332f 100644 --- a/pkg/driverutil/driverutil.go +++ b/pkg/driverutil/driverutil.go @@ -3,6 +3,7 @@ package driverutil import ( "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/vz" + "github.com/lima-vm/lima/pkg/wsl2" ) // Drivers returns the available drivers. @@ -11,5 +12,8 @@ func Drivers() []string { if vz.Enabled { drivers = append(drivers, limayaml.VZ) } + if wsl2.Enabled { + drivers = append(drivers, limayaml.WSL2) + } return drivers } diff --git a/pkg/driverutil/instance.go b/pkg/driverutil/instance.go index 891153ac11de..09760faa76c4 100644 --- a/pkg/driverutil/instance.go +++ b/pkg/driverutil/instance.go @@ -5,6 +5,7 @@ import ( "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/lima/pkg/qemu" "github.com/lima-vm/lima/pkg/vz" + "github.com/lima-vm/lima/pkg/wsl2" ) func CreateTargetDriverInstance(base *driver.BaseDriver) driver.Driver { @@ -12,5 +13,8 @@ func CreateTargetDriverInstance(base *driver.BaseDriver) driver.Driver { if *limaDriver == limayaml.VZ { return vz.New(base) } + if *limaDriver == limayaml.WSL2 { + return wsl2.New(base) + } return qemu.New(base) } diff --git a/pkg/executil/command.go b/pkg/executil/command.go new file mode 100644 index 000000000000..fb6e59497e01 --- /dev/null +++ b/pkg/executil/command.go @@ -0,0 +1,51 @@ +package executil + +import ( + "bytes" + "context" + "fmt" + "os/exec" + + "github.com/lima-vm/lima/pkg/ioutilx" +) + +type options struct { + ctx *context.Context +} + +type Opt func(*options) error + +// WithContext runs the command with CommandContext. +func WithContext(ctx *context.Context) Opt { + return func(o *options) error { + o.ctx = ctx + return nil + } +} + +func RunUTF16leCommand(args []string, opts ...Opt) (string, error) { + var o options + for _, f := range opts { + if err := f(&o); err != nil { + return "", err + } + } + + var cmd *exec.Cmd + if o.ctx != nil { + cmd = exec.CommandContext(*o.ctx, args[0], args[1:]...) + } else { + cmd = exec.Command(args[0], args[1:]...) + } + + outString := "" + out, err := cmd.CombinedOutput() + if out != nil { + s, err := ioutilx.FromUTF16leToString(bytes.NewReader(out)) + if err != nil { + return "", fmt.Errorf("failed to convert output from UTF16 when running command %v, err: %w", args, err) + } + outString = s + } + return outString, err +} diff --git a/pkg/guestagent/api/client/client.go b/pkg/guestagent/api/client/client.go index 285abe926ea9..d58aaeefe194 100644 --- a/pkg/guestagent/api/client/client.go +++ b/pkg/guestagent/api/client/client.go @@ -7,7 +7,9 @@ import ( "context" "encoding/json" "fmt" + "net" "net/http" + "strconv" "github.com/lima-vm/lima/pkg/guestagent/api" "github.com/lima-vm/lima/pkg/httpclientutil" @@ -19,13 +21,39 @@ type GuestAgentClient interface { Events(context.Context, func(api.Event)) error } +type Proto = string + +const ( + UNIX Proto = "unix" + VSOCK Proto = "vsock" +) + // NewGuestAgentClient creates a client. -// socketPath is a path to the UNIX socket, without unix:// prefix. -func NewGuestAgentClient(socketPath string) (GuestAgentClient, error) { - hc, err := httpclientutil.NewHTTPClientWithSocketPath(socketPath) - if err != nil { - return nil, err +// remote is a path to the UNIX socket, without unix:// prefix or a remote hostname/IP address. +func NewGuestAgentClient(remote string, proto Proto, instanceName string) (GuestAgentClient, error) { + var hc *http.Client + switch proto { + case UNIX: + hcSock, err := httpclientutil.NewHTTPClientWithSocketPath(remote) + if err != nil { + return nil, err + } + hc = hcSock + case VSOCK: + _, p, err := net.SplitHostPort(remote) + if err != nil { + return nil, err + } + port, err := strconv.Atoi(p) + if err != nil { + return nil, err + } + hc, err = newVSockGuestAgentClient(port, instanceName) + if err != nil { + return nil, err + } } + return NewGuestAgentClientWithHTTPClient(hc), nil } diff --git a/pkg/guestagent/api/client/client_others.go b/pkg/guestagent/api/client/client_others.go new file mode 100644 index 000000000000..7fcbdb184192 --- /dev/null +++ b/pkg/guestagent/api/client/client_others.go @@ -0,0 +1,16 @@ +//go:build !windows +// +build !windows + +package client + +import ( + "net/http" + + "github.com/lima-vm/lima/pkg/httpclientutil" +) + +func newVSockGuestAgentClient(port int, _ string) (*http.Client, error) { + hc := httpclientutil.NewHTTPClientWithVSockPort(port) + + return hc, nil +} diff --git a/pkg/guestagent/api/client/client_windows.go b/pkg/guestagent/api/client/client_windows.go new file mode 100644 index 000000000000..7dc19658b646 --- /dev/null +++ b/pkg/guestagent/api/client/client_windows.go @@ -0,0 +1,19 @@ +//go:build windows +// +build windows + +package client + +import ( + "net/http" + + "github.com/lima-vm/lima/pkg/httpclientutil" +) + +func newVSockGuestAgentClient(port int, instanceName string) (*http.Client, error) { + hc, err := httpclientutil.NewHTTPClientWithVSockPort(instanceName, port) + if err != nil { + return nil, err + } + + return hc, nil +} diff --git a/pkg/guestagent/guestagent_linux.go b/pkg/guestagent/guestagent_linux.go index 1807bf6dde14..889a46a556e3 100644 --- a/pkg/guestagent/guestagent_linux.go +++ b/pkg/guestagent/guestagent_linux.go @@ -5,6 +5,7 @@ import ( "errors" "reflect" "sync" + "syscall" "time" "github.com/elastic/go-libaudit/v2" @@ -25,9 +26,17 @@ func New(newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration } auditClient, err := libaudit.NewMulticastAuditClient(nil) - if err != nil { + switch { + case errors.Is(err, syscall.EPROTONOSUPPORT), errors.Is(err, syscall.EAFNOSUPPORT): + // system doesn't support auditing, skip + a.worthCheckingIPTables = true + go a.kubernetesServiceWatcher.Start() + go a.fixSystemTimeSkew() + return a, nil + case !errors.Is(err, nil): return nil, err } + auditStatus, err := auditClient.GetStatus() if err != nil { return nil, err @@ -36,9 +45,20 @@ func New(newTicker func() (<-chan time.Time, func()), iptablesIdle time.Duration if err = auditClient.SetEnabled(true, libaudit.WaitForReply); err != nil { return nil, err } - } + auditStatus, err := auditClient.GetStatus() + if err != nil { + return nil, err + } + if auditStatus.Enabled == 0 { + if err = auditClient.SetEnabled(true, libaudit.WaitForReply); err != nil { + return nil, err + } + } - go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle) + go a.setWorthCheckingIPTablesRoutine(auditClient, iptablesIdle) + } else { + a.worthCheckingIPTables = true + } go a.kubernetesServiceWatcher.Start() go a.fixSystemTimeSkew() return a, nil diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index bfb2dc6c1da7..1c5aeb7fa01c 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -42,15 +42,19 @@ type HostAgent struct { tcpDNSLocalPort int instDir string instName string + instSSHAddress string sshConfig *ssh.SSHConfig portForwarder *portForwarder onClose []func() error // LIFO + guestAgentProto guestagentclient.Proto driver driver.Driver sigintCh chan os.Signal eventEnc *json.Encoder eventEncMu sync.Mutex + + vSockPort int } type options struct { @@ -91,6 +95,9 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt if err != nil { return nil, err } + if *y.VMType == limayaml.WSL2 { + sshLocalPort = inst.SSHLocalPort + } var udpDNSLocalPort, tcpDNSLocalPort int if *y.HostResolver.Enabled { @@ -104,7 +111,21 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt } } - if err := cidata.GenerateISO9660(inst.Dir, instName, y, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive); err != nil { + guestAgentProto := guestagentclient.UNIX + if *y.VMType == limayaml.WSL2 { + guestAgentProto = guestagentclient.VSOCK + } + + vSockPort := 0 + if guestAgentProto == guestagentclient.VSOCK { + port, err := getFreeVSockPort() + if err != nil { + logrus.WithError(err).Error("failed to get free VSock port") + } + vSockPort = port + } + + if err := cidata.GenerateISO9660(inst.Dir, instName, y, udpDNSLocalPort, tcpDNSLocalPort, o.nerdctlArchive, vSockPort); err != nil { return nil, err } @@ -112,7 +133,7 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt if err != nil { return nil, err } - if err = writeSSHConfigFile(inst, sshLocalPort, sshOpts); err != nil { + if err = writeSSHConfigFile(inst, inst.SSHAddress, sshLocalPort, sshOpts); err != nil { return nil, err } sshConfig := &ssh.SSHConfig{ @@ -145,16 +166,19 @@ func New(instName string, stdout io.Writer, sigintCh chan os.Signal, opts ...Opt tcpDNSLocalPort: tcpDNSLocalPort, instDir: inst.Dir, instName: instName, + instSSHAddress: inst.SSHAddress, sshConfig: sshConfig, - portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules), + portForwarder: newPortForwarder(sshConfig, sshLocalPort, rules, inst.VMType), driver: limaDriver, sigintCh: sigintCh, eventEnc: json.NewEncoder(stdout), + vSockPort: vSockPort, + guestAgentProto: guestAgentProto, } return a, nil } -func writeSSHConfigFile(inst *store.Instance, sshLocalPort int, sshOpts []string) error { +func writeSSHConfigFile(inst *store.Instance, instSSHAddress string, sshLocalPort int, sshOpts []string) error { if inst.Dir == "" { return fmt.Errorf("directory is unknown for the instance %q", inst.Name) } @@ -167,7 +191,7 @@ func writeSSHConfigFile(inst *store.Instance, sshLocalPort int, sshOpts []string } if err := sshutil.Format(&b, inst.Name, sshutil.FormatConfig, append(sshOpts, - "Hostname=127.0.0.1", + fmt.Sprintf("Hostname=%s", instSSHAddress), fmt.Sprintf("Port=%d", sshLocalPort), )); err != nil { return err @@ -291,6 +315,15 @@ func (a *HostAgent) Run(ctx context.Context) error { return err } + // WSL instance SSH address isn't known until after VM start + if *a.y.VMType == limayaml.WSL2 { + sshAddr, err := store.GetSSHAddress(a.instName) + if err != nil { + return err + } + a.instSSHAddress = sshAddr + } + if a.y.Video.Display != nil && *a.y.Video.Display == "vnc" { vncdisplay, vncoptions, _ := strings.Cut(*a.y.Video.VNC.Display, ",") vnchost, vncnum, err := net.SplitHostPort(vncdisplay) @@ -395,7 +428,7 @@ func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { func (a *HostAgent) startHostAgentRoutines(ctx context.Context) error { a.onClose = append(a.onClose, func() error { logrus.Debugf("shutting down the SSH master") - if exitMasterErr := ssh.ExitMaster("127.0.0.1", a.sshLocalPort, a.sshConfig); exitMasterErr != nil { + if exitMasterErr := ssh.ExitMaster(a.instSSHAddress, a.sshLocalPort, a.sshConfig); exitMasterErr != nil { logrus.WithError(exitMasterErr).Warn("failed to exit SSH master") } return nil @@ -411,7 +444,7 @@ sudo mkdir -p -m 700 /run/host-services sudo ln -sf "${SSH_AUTH_SOCK}" /run/host-services/ssh-auth.sock sudo chown -R "${USER}" /run/host-services` faDesc := "linking ssh auth socket to static location /run/host-services/ssh-auth.sock" - stdout, stderr, err := ssh.ExecuteScript("127.0.0.1", a.sshLocalPort, a.sshConfig, faScript, faDesc) + stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, faScript, faDesc) logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) if err != nil { errs = append(errs, fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err)) @@ -492,18 +525,20 @@ func (a *HostAgent) close() error { func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { // TODO: use vSock (when QEMU for macOS gets support for vSock) + localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) + remoteUnix := "/run/lima-guestagent.sock" + // Setup all socket forwards and defer their teardown - logrus.Debugf("Forwarding unix sockets") - for _, rule := range a.y.PortForwards { - if rule.GuestSocket != "" { - local := hostAddress(rule, guestagentapi.IPPort{}) - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, rule.GuestSocket, verbForward, rule.Reverse) + if *a.y.VMType != limayaml.WSL2 { + logrus.Debugf("Forwarding unix sockets") + for _, rule := range a.y.PortForwards { + if rule.GuestSocket != "" { + local := hostAddress(rule, guestagentapi.IPPort{}) + _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, local, localUnix, verbForward, rule.Reverse) + } } } - localUnix := filepath.Join(a.instDir, filenames.GuestAgentSock) - remoteUnix := "/run/lima-guestagent.sock" - a.onClose = append(a.onClose, func() error { logrus.Debugf("Stop forwarding unix sockets") var errs []error @@ -522,11 +557,18 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { return errors.Join(errs...) }) + guestSocketAddr := localUnix + if a.guestAgentProto == guestagentclient.VSOCK { + guestSocketAddr = fmt.Sprintf("0.0.0.0:%d", a.vSockPort) + } + for { - if !isGuestAgentSocketAccessible(ctx, localUnix) { - _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) + if !isGuestAgentSocketAccessible(ctx, guestSocketAddr, a.guestAgentProto, a.instName) { + if a.guestAgentProto != guestagentclient.VSOCK { + _ = forwardSSH(ctx, a.sshConfig, a.sshLocalPort, localUnix, remoteUnix, verbForward, false) + } } - if err := a.processGuestAgentEvents(ctx, localUnix); err != nil { + if err := a.processGuestAgentEvents(ctx, guestSocketAddr, a.guestAgentProto, a.instName); err != nil { if !errors.Is(err, context.Canceled) { logrus.WithError(err).Warn("connection to the guest agent was closed unexpectedly") } @@ -539,8 +581,8 @@ func (a *HostAgent) watchGuestAgentEvents(ctx context.Context) { } } -func isGuestAgentSocketAccessible(ctx context.Context, localUnix string) bool { - client, err := guestagentclient.NewGuestAgentClient(localUnix) +func isGuestAgentSocketAccessible(ctx context.Context, localUnix string, proto guestagentclient.Proto, instanceName string) bool { + client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName) if err != nil { return false } @@ -548,8 +590,8 @@ func isGuestAgentSocketAccessible(ctx context.Context, localUnix string) bool { return err == nil } -func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix string) error { - client, err := guestagentclient.NewGuestAgentClient(localUnix) +func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix string, proto guestagentclient.Proto, instanceName string) error { + client, err := guestagentclient.NewGuestAgentClient(localUnix, proto, instanceName) if err != nil { return err } @@ -566,7 +608,7 @@ func (a *HostAgent) processGuestAgentEvents(ctx context.Context, localUnix strin for _, f := range ev.Errors { logrus.Warnf("received error from the guest: %q", f) } - a.portForwarder.OnEvent(ctx, ev) + a.portForwarder.OnEvent(ctx, ev, a.instSSHAddress) } if err := client.Events(ctx, onEvent); err != nil { diff --git a/pkg/hostagent/port.go b/pkg/hostagent/port.go index 31edb0e76987..772f951bd97c 100644 --- a/pkg/hostagent/port.go +++ b/pkg/hostagent/port.go @@ -14,15 +14,17 @@ type portForwarder struct { sshConfig *ssh.SSHConfig sshHostPort int rules []limayaml.PortForward + vmType limayaml.VMType } const sshGuestPort = 22 -func newPortForwarder(sshConfig *ssh.SSHConfig, sshHostPort int, rules []limayaml.PortForward) *portForwarder { +func newPortForwarder(sshConfig *ssh.SSHConfig, sshHostPort int, rules []limayaml.PortForward, vmType limayaml.VMType) *portForwarder { return &portForwarder{ sshConfig: sshConfig, sshHostPort: sshHostPort, rules: rules, + vmType: vmType, } } @@ -40,7 +42,15 @@ func hostAddress(rule limayaml.PortForward, guest api.IPPort) string { return host.String() } -func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string) { +func (pf *portForwarder) forwardingAddresses(guest api.IPPort, localUnixIP net.IP) (string, string) { + if pf.vmType == limayaml.WSL2 { + guest.IP = localUnixIP + host := api.IPPort{ + IP: net.ParseIP("127.0.0.1"), + Port: guest.Port, + } + return host.String(), guest.String() + } for _, rule := range pf.rules { if rule.GuestSocket != "" { continue @@ -69,9 +79,11 @@ func (pf *portForwarder) forwardingAddresses(guest api.IPPort) (string, string) return "", guest.String() } -func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) { +func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event, instSSHAddress string) { + localUnixIP := net.ParseIP(instSSHAddress) + for _, f := range ev.LocalPortsRemoved { - local, remote := pf.forwardingAddresses(f) + local, remote := pf.forwardingAddresses(f, localUnixIP) if local == "" { continue } @@ -81,7 +93,7 @@ func (pf *portForwarder) OnEvent(ctx context.Context, ev api.Event) { } } for _, f := range ev.LocalPortsAdded { - local, remote := pf.forwardingAddresses(f) + local, remote := pf.forwardingAddresses(f, localUnixIP) if local == "" { logrus.Infof("Not forwarding TCP %s", remote) continue diff --git a/pkg/hostagent/port_darwin.go b/pkg/hostagent/port_darwin.go index f004b9a13344..bd889674e6c5 100644 --- a/pkg/hostagent/port_darwin.go +++ b/pkg/hostagent/port_darwin.go @@ -156,3 +156,7 @@ func (plf *pseudoLoopbackForwarder) Close() error { _ = plf.ln.Close() return plf.onClose() } + +func getFreeVSockPort() (int, error) { + return 0, nil +} diff --git a/pkg/hostagent/port_others.go b/pkg/hostagent/port_others.go index 35d276d6f316..601fb035ee9a 100644 --- a/pkg/hostagent/port_others.go +++ b/pkg/hostagent/port_others.go @@ -1,5 +1,5 @@ -//go:build !darwin -// +build !darwin +//go:build !darwin && !windows +// +build !darwin,!windows package hostagent @@ -12,3 +12,7 @@ import ( func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error { return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) } + +func getFreeVSockPort() (int, error) { + return 0, nil +} diff --git a/pkg/hostagent/port_windows.go b/pkg/hostagent/port_windows.go new file mode 100644 index 000000000000..f0917c2ba0cb --- /dev/null +++ b/pkg/hostagent/port_windows.go @@ -0,0 +1,19 @@ +//go:build windows +// +build windows + +package hostagent + +import ( + "context" + + "github.com/lima-vm/lima/pkg/windows" + "github.com/lima-vm/sshocker/pkg/ssh" +) + +func forwardTCP(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string, verb string) error { + return forwardSSH(ctx, sshConfig, port, local, remote, verb, false) +} + +func getFreeVSockPort() (int, error) { + return windows.GetRandomFreeVSockPort(0, 2147483647) +} diff --git a/pkg/hostagent/requirements.go b/pkg/hostagent/requirements.go index 53e6b01055b9..46057ed3c0b6 100644 --- a/pkg/hostagent/requirements.go +++ b/pkg/hostagent/requirements.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + guestagentclient "github.com/lima-vm/lima/pkg/guestagent/api/client" "github.com/lima-vm/lima/pkg/limayaml" "github.com/lima-vm/sshocker/pkg/ssh" "github.com/sirupsen/logrus" @@ -43,7 +44,7 @@ func (a *HostAgent) waitForRequirements(label string, requirements []requirement func (a *HostAgent) waitForRequirement(r requirement) error { logrus.Debugf("executing script %q", r.description) - stdout, stderr, err := ssh.ExecuteScript("127.0.0.1", a.sshLocalPort, a.sshConfig, r.script, r.description) + stdout, stderr, err := ssh.ExecuteScript(a.instSSHAddress, a.sshLocalPort, a.sshConfig, r.script, r.description) logrus.Debugf("stdout=%q, stderr=%q, err=%v", stdout, stderr, err) if err != nil { return fmt.Errorf("stdout=%q, stderr=%q: %w", stdout, stderr, err) @@ -116,9 +117,26 @@ fi }) } - req = append(req, requirement{ - description: "the guest agent to be running", - script: `#!/bin/bash + if a.guestAgentProto == guestagentclient.VSOCK { + req = append(req, requirement{ + description: "the guest agent to be running", + script: fmt.Sprintf(`#!/bin/bash +set -eux -o pipefail +if ! timeout 30s bash -c "until ss -a -n --vsock --listen | grep -q '*:%d'; do sleep 3; done"; then + echo >&2 "lima-guestagent is not installed yet" + exit 1 +fi +`, a.vSockPort), + debugHint: `The guest agent (/run/lima-guestagent.sock) does not seem running. +Make sure that you are using an officially supported image. +Also see "/var/log/cloud-init-output.log" in the guest. +A possible workaround is to run "lima-guestagent install-systemd" in the guest. +`, + }) + } else { + req = append(req, requirement{ + description: "the guest agent to be running", + script: `#!/bin/bash set -eux -o pipefail sock="/run/lima-guestagent.sock" if ! timeout 30s bash -c "until [ -S \"${sock}\" ]; do sleep 3; done"; then @@ -126,12 +144,13 @@ if ! timeout 30s bash -c "until [ -S \"${sock}\" ]; do sleep 3; done"; then exit 1 fi `, - debugHint: `The guest agent (/run/lima-guestagent.sock) does not seem running. + debugHint: `The guest agent (/run/lima-guestagent.sock) does not seem running. Make sure that you are using an officially supported image. Also see "/var/log/cloud-init-output.log" in the guest. A possible workaround is to run "lima-guestagent install-systemd" in the guest. `, - }) + }) + } return req } diff --git a/pkg/httpclientutil/httpclientutil.go b/pkg/httpclientutil/httpclientutil.go index 4e398dbec5b6..90b6c3fecbf8 100644 --- a/pkg/httpclientutil/httpclientutil.go +++ b/pkg/httpclientutil/httpclientutil.go @@ -9,30 +9,11 @@ import ( "errors" "fmt" "io" - "net" "net/http" - "os" "github.com/lima-vm/lima/pkg/httputil" ) -// NewHTTPClientWithSocketPath creates a client. -// socketPath is a path to the UNIX socket, without unix:// prefix. -func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { - if _, err := os.Stat(socketPath); err != nil { - return nil, err - } - hc := &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - var d net.Dialer - return d.DialContext(ctx, "unix", socketPath) - }, - }, - } - return hc, nil -} - // Get calls HTTP GET and verifies that the status code is 2XX . func Get(ctx context.Context, c *http.Client, url string) (*http.Response, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) diff --git a/pkg/httpclientutil/httpclientutil_others.go b/pkg/httpclientutil/httpclientutil_others.go new file mode 100644 index 000000000000..a90b8ee76415 --- /dev/null +++ b/pkg/httpclientutil/httpclientutil_others.go @@ -0,0 +1,43 @@ +//go:build !windows +// +build !windows + +package httpclientutil + +import ( + "context" + "net" + "net/http" + "os" + + "github.com/mdlayher/vsock" +) + +// NewHTTPClientWithSocketPath creates a client. +// socketPath is a path to the UNIX socket, without unix:// prefix. +func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { + if _, err := os.Stat(socketPath); err != nil { + return nil, err + } + hc := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", socketPath) + }, + }, + } + return hc, nil +} + +// NewHTTPClientWithVSockPort creates a client. +// port is the port to use for the vsock. +func NewHTTPClientWithVSockPort(port int) *http.Client { + hc := &http.Client{ + Transport: &http.Transport{ + Dial: func(_, _ string) (net.Conn, error) { + return vsock.Dial(2, uint32(port), nil) + }, + }, + } + return hc +} diff --git a/pkg/httpclientutil/httpclientutil_windows.go b/pkg/httpclientutil/httpclientutil_windows.go new file mode 100644 index 000000000000..5c84c1ed0e8c --- /dev/null +++ b/pkg/httpclientutil/httpclientutil_windows.go @@ -0,0 +1,65 @@ +//go:build windows +// +build windows + +package httpclientutil + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + + winio "github.com/Microsoft/go-winio" + "github.com/Microsoft/go-winio/pkg/guid" + "github.com/lima-vm/lima/pkg/windows" +) + +// NewHTTPClientWithSocketPath creates a client. +// socketPath is a path to the UNIX socket, without unix:// prefix. +func NewHTTPClientWithSocketPath(socketPath string) (*http.Client, error) { + // Use Lstat on windows, see: https://github.com/adrg/xdg/pull/14 + if _, err := os.Lstat(socketPath); err != nil { + return nil, err + } + hc := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + var d net.Dialer + return d.DialContext(ctx, "unix", socketPath) + }, + }, + } + return hc, nil +} + +// NewHTTPClientWithVSockPort creates a client for a vsock port. +func NewHTTPClientWithVSockPort(instanceName string, port int) (*http.Client, error) { + VMIDStr, err := windows.GetInstanceVMID(fmt.Sprintf("lima-%s", instanceName)) + if err != nil { + return nil, err + } + VMIDGUID, err := guid.FromString(VMIDStr) + if err != nil { + return nil, err + } + + serviceGUID, err := guid.FromString(fmt.Sprintf("%x%s", port, windows.MagicVSOCKSuffix)) + if err != nil { + return nil, err + } + + sockAddr := &winio.HvsockAddr{ + VMID: VMIDGUID, + ServiceID: serviceGUID, + } + + hc := &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + return winio.Dial(ctx, sockAddr) + }, + }, + } + return hc, nil +} diff --git a/pkg/ioutilx/ioutilx.go b/pkg/ioutilx/ioutilx.go index fa8a38ee56fd..ceee1c2fdd6d 100644 --- a/pkg/ioutilx/ioutilx.go +++ b/pkg/ioutilx/ioutilx.go @@ -4,6 +4,12 @@ import ( "errors" "fmt" "io" + "os/exec" + "strings" + + "github.com/sirupsen/logrus" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" ) // ReadAtMaximum reands n at maximum. @@ -20,3 +26,32 @@ func ReadAtMaximum(r io.Reader, n int64) ([]byte, error) { } return b, err } + +// FromUTF16le returns an io.Reader for UTF16le data. +// Windows uses little endian by default, use unicode.UseBOM policy to retrieve BOM from the text, +// and unicode.LittleEndian as a fallback +func FromUTF16le(r io.Reader) io.Reader { + o := transform.NewReader(r, unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()) + return o +} + +// FromUTF16leToString reads from Unicode 16 LE encoded data from an io.Reader and returns a string. +func FromUTF16leToString(r io.Reader) (string, error) { + out, err := io.ReadAll(FromUTF16le(r)) + if err != nil { + return "", err + } + + return string(out), nil +} + +func CanonicalWindowsPath(orig string) string { + newPath := orig + out, err := exec.Command("cygpath", "-m", orig).CombinedOutput() + if err != nil { + logrus.WithError(err).Errorf("failed to convert path to mingw, maybe not using Git ssh?") + } else { + newPath = strings.TrimSpace(string(out)) + } + return newPath +} diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go index 910fe21b472c..94473925884e 100644 --- a/pkg/limayaml/defaults.go +++ b/pkg/limayaml/defaults.go @@ -828,6 +828,8 @@ func NewVMType(driver string) VMType { return VZ case "qemu": return QEMU + case "wsl2": + return WSL2 default: logrus.Warnf("Unknown driver: %s", driver) return driver diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go index 990ef0a4dd4f..f79e902c0923 100644 --- a/pkg/limayaml/limayaml.go +++ b/pkg/limayaml/limayaml.go @@ -56,9 +56,11 @@ const ( REVSSHFS MountType = "reverse-sshfs" NINEP MountType = "9p" VIRTIOFS MountType = "virtiofs" + WSLMount MountType = "wsl2" QEMU VMType = "qemu" VZ VMType = "vz" + WSL2 VMType = "wsl2" ) type Rosetta struct { diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go index 7495216d38a2..b3b8a02f7b65 100644 --- a/pkg/limayaml/validate.go +++ b/pkg/limayaml/validate.go @@ -56,12 +56,14 @@ func Validate(y LimaYAML, warn bool) error { switch *y.VMType { case QEMU: // NOP + case WSL2: + // NOP case VZ: if !IsNativeArch(*y.Arch) { return fmt.Errorf("field `arch` must be %q for VZ; got %q", NewArch(runtime.GOARCH), *y.Arch) } default: - return fmt.Errorf("field `vmType` must be %q or %q; got %q", QEMU, VZ, *y.VMType) + return fmt.Errorf("field `vmType` must be %q, %q, %q; got %q", QEMU, VZ, WSL2, *y.VMType) } if len(y.Images) == 0 { @@ -159,9 +161,9 @@ func Validate(y LimaYAML, warn bool) error { } switch *y.MountType { - case REVSSHFS, NINEP, VIRTIOFS: + case REVSSHFS, NINEP, VIRTIOFS, WSLMount: default: - return fmt.Errorf("field `mountType` must be %q or %q or %q, got %q", REVSSHFS, NINEP, VIRTIOFS, *y.MountType) + return fmt.Errorf("field `mountType` must be %q or %q or %q, or %q, got %q", REVSSHFS, NINEP, VIRTIOFS, WSLMount, *y.MountType) } if warn && runtime.GOOS != "linux" { diff --git a/pkg/osutil/osutil_windows.go b/pkg/osutil/osutil_windows.go index 816731eab6b6..48a109ad0f6b 100644 --- a/pkg/osutil/osutil_windows.go +++ b/pkg/osutil/osutil_windows.go @@ -3,6 +3,9 @@ package osutil import ( "fmt" "io/fs" + "syscall" + + "golang.org/x/sys/windows" ) // UnixPathMax is the value of UNIX_PATH_MAX. @@ -14,7 +17,7 @@ type Stat struct { Gid uint32 //nolint:revive } -func SysStat(fi fs.FileInfo) (Stat, bool) { +func SysStat(_ fs.FileInfo) (Stat, bool) { return Stat{Uid: 0, Gid: 0}, false } @@ -26,10 +29,10 @@ const SigKill = Signal(9) type Signal int -func SysKill(pid int, sig Signal) error { - return fmt.Errorf("unimplemented") +func SysKill(pid int, _ Signal) error { + return windows.GenerateConsoleCtrlEvent(syscall.CTRL_BREAK_EVENT, uint32(pid)) } -func Ftruncate(fd int, length int64) (err error) { +func Ftruncate(_ int, _ int64) (err error) { return fmt.Errorf("unimplemented") } diff --git a/pkg/sshutil/sshutil.go b/pkg/sshutil/sshutil.go index 8cc25b632111..33f79f3762fc 100644 --- a/pkg/sshutil/sshutil.go +++ b/pkg/sshutil/sshutil.go @@ -16,6 +16,7 @@ import ( "sync" "github.com/coreos/go-semver/semver" + "github.com/lima-vm/lima/pkg/ioutilx" "github.com/lima-vm/lima/pkg/lockutil" "github.com/lima-vm/lima/pkg/osutil" "github.com/lima-vm/lima/pkg/store/dirnames" @@ -134,7 +135,13 @@ func CommonOpts(useDotSSH bool) ([]string, error) { if err != nil { return nil, err } - opts := []string{"IdentityFile=\"" + privateKeyPath + "\""} + var opts []string + if runtime.GOOS == "windows" { + privateKeyPath = ioutilx.CanonicalWindowsPath(privateKeyPath) + opts = []string{"IdentityFile=" + privateKeyPath} + } else { + opts = []string{"IdentityFile=\"" + privateKeyPath + "\""} + } // Append all private keys corresponding to ~/.ssh/*.pub to keep old instances working // that had been created before lima started using an internal identity. @@ -193,10 +200,18 @@ func CommonOpts(useDotSSH bool) ([]string, error) { // We prioritize AES algorithms when AES accelerator is available. if sshInfo.aesAccelerated { logrus.Debugf("AES accelerator seems available, prioritizing aes128-gcm@openssh.com and aes256-gcm@openssh.com") - opts = append(opts, "Ciphers=\"^aes128-gcm@openssh.com,aes256-gcm@openssh.com\"") + if runtime.GOOS == "windows" { + opts = append(opts, "Ciphers=^aes128-gcm@openssh.com,aes256-gcm@openssh.com") + } else { + opts = append(opts, "Ciphers=\"^aes128-gcm@openssh.com,aes256-gcm@openssh.com\"") + } } else { logrus.Debugf("AES accelerator does not seem available, prioritizing chacha20-poly1305@openssh.com") - opts = append(opts, "Ciphers=\"^chacha20-poly1305@openssh.com\"") + if runtime.GOOS == "windows" { + opts = append(opts, "Ciphers=^chacha20-poly1305@openssh.com") + } else { + opts = append(opts, "Ciphers=\"^chacha20-poly1305@openssh.com\"") + } } } return opts, nil @@ -216,11 +231,16 @@ func SSHOpts(instDir string, useDotSSH, forwardAgent bool, forwardX11 bool, forw if err != nil { return nil, err } + controlPath := fmt.Sprintf("ControlPath=\"%s\"", controlSock) + if runtime.GOOS == "windows" { + controlSock = ioutilx.CanonicalWindowsPath(controlSock) + controlPath = fmt.Sprintf("ControlPath=%s", controlSock) + } opts = append(opts, fmt.Sprintf("User=%s", u.Username), // guest and host have the same username, but we should specify the username explicitly (#85) "ControlMaster=auto", - fmt.Sprintf("ControlPath=\"%s\"", controlSock), - "ControlPersist=5m", + controlPath, + "ControlPersist=yes", ) if forwardAgent { opts = append(opts, "ForwardAgent=yes") diff --git a/pkg/start/ha_cmd_opts_others.go b/pkg/start/ha_cmd_opts_others.go new file mode 100644 index 000000000000..60f7f1f1e6dc --- /dev/null +++ b/pkg/start/ha_cmd_opts_others.go @@ -0,0 +1,10 @@ +//go:build !windows +// +build !windows + +package start + +import ( + "syscall" +) + +var SysProcAttr = &syscall.SysProcAttr{} diff --git a/pkg/start/ha_cmd_opts_windows.go b/pkg/start/ha_cmd_opts_windows.go new file mode 100644 index 000000000000..047b8eb32394 --- /dev/null +++ b/pkg/start/ha_cmd_opts_windows.go @@ -0,0 +1,9 @@ +package start + +import ( + "syscall" +) + +var SysProcAttr = &syscall.SysProcAttr{ + CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP, +} diff --git a/pkg/start/start.go b/pkg/start/start.go index 1e223ed9cbc2..2df500907a6f 100644 --- a/pkg/start/start.go +++ b/pkg/start/start.go @@ -160,6 +160,7 @@ func Start(ctx context.Context, inst *store.Instance) error { } args = append(args, inst.Name) haCmd := exec.CommandContext(ctx, self, args...) + haCmd.SysProcAttr = SysProcAttr haCmd.Stdout = haStdoutW haCmd.Stderr = haStderrW diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go index d3dd43dcfb86..297ddfaa9661 100644 --- a/pkg/store/filenames/filenames.go +++ b/pkg/store/filenames/filenames.go @@ -28,6 +28,7 @@ const ( const ( LimaYAML = "lima.yaml" CIDataISO = "cidata.iso" + CIDataISODir = "cidata" BaseDisk = "basedisk" DiffDisk = "diffdisk" Kernel = "kernel" diff --git a/pkg/store/instance.go b/pkg/store/instance.go index af9d3f18a1a2..cc3f3387e441 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -27,10 +27,12 @@ import ( type Status = string const ( - StatusUnknown Status = "" - StatusBroken Status = "Broken" - StatusStopped Status = "Stopped" - StatusRunning Status = "Running" + StatusUnknown Status = "" + StatusUninitialized Status = "Uninitialized" + StatusInstalling Status = "Installing" + StatusBroken Status = "Broken" + StatusStopped Status = "Stopped" + StatusRunning Status = "Running" ) type Instance struct { @@ -52,6 +54,7 @@ type Instance struct { DriverPID int `json:"driverPID,omitempty"` Errors []error `json:"errors,omitempty"` Config *limayaml.LimaYAML `json:"config,omitempty"` + SSHAddress string `json:"sshAddress,omitempty"` } func (inst *Instance) LoadYAML() (*limayaml.LimaYAML, error) { @@ -89,21 +92,9 @@ func Inspect(instName string) (*Instance, error) { inst.Arch = *y.Arch inst.VMType = *y.VMType inst.CPUType = y.CPUType[*y.Arch] - - inst.CPUs = *y.CPUs - memory, err := units.RAMInBytes(*y.Memory) - if err == nil { - inst.Memory = memory - } - disk, err := units.RAMInBytes(*y.Disk) - if err == nil { - inst.Disk = disk - } - inst.AdditionalDisks = y.AdditionalDisks - inst.Networks = y.Networks + inst.SSHAddress = "127.0.0.1" inst.SSHLocalPort = *y.SSH.LocalPort // maybe 0 inst.SSHConfigFile = filepath.Join(instDir, filenames.SSHConfig) - inst.HostAgentPID, err = ReadPIDFile(filepath.Join(instDir, filenames.HostAgentPID)) if err != nil { inst.Status = StatusBroken @@ -129,26 +120,27 @@ func Inspect(instName string) (*Instance, error) { } } - inst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType))) - if err != nil { - inst.Status = StatusBroken - inst.Errors = append(inst.Errors, err) + inst.CPUs = *y.CPUs + memory, err := units.RAMInBytes(*y.Memory) + if err == nil { + inst.Memory = memory } + disk, err := units.RAMInBytes(*y.Disk) + if err == nil { + inst.Disk = disk + } + inst.AdditionalDisks = y.AdditionalDisks + inst.Networks = y.Networks - if inst.Status == StatusUnknown { - if inst.HostAgentPID > 0 && inst.DriverPID > 0 { - inst.Status = StatusRunning - } else if inst.HostAgentPID == 0 && inst.DriverPID == 0 { - inst.Status = StatusStopped - } else if inst.HostAgentPID > 0 && inst.DriverPID == 0 { - inst.Errors = append(inst.Errors, errors.New("host agent is running but driver is not")) - inst.Status = StatusBroken - } else { - inst.Errors = append(inst.Errors, fmt.Errorf("%s driver is running but host agent is not", inst.VMType)) - inst.Status = StatusBroken - } + // 0 out values since not configurable on WSL2 + if inst.VMType == limayaml.WSL2 { + inst.Memory = 0 + inst.CPUs = 0 + inst.Disk = 0 } + inspectStatus(instDir, inst, y) + tmpl, err := template.New("format").Parse(y.Message) if err != nil { inst.Errors = append(inst.Errors, fmt.Errorf("message %q is not a valid template: %w", y.Message, err)) @@ -172,6 +164,29 @@ func Inspect(instName string) (*Instance, error) { return inst, nil } +func inspectStatusWithPIDFiles(instDir string, inst *Instance, y *limayaml.LimaYAML) { + var err error + inst.DriverPID, err = ReadPIDFile(filepath.Join(instDir, filenames.PIDFile(*y.VMType))) + if err != nil { + inst.Status = StatusBroken + inst.Errors = append(inst.Errors, err) + } + + if inst.Status == StatusUnknown { + if inst.HostAgentPID > 0 && inst.DriverPID > 0 { + inst.Status = StatusRunning + } else if inst.HostAgentPID == 0 && inst.DriverPID == 0 { + inst.Status = StatusStopped + } else if inst.HostAgentPID > 0 && inst.DriverPID == 0 { + inst.Errors = append(inst.Errors, errors.New("host agent is running but driver is not")) + inst.Status = StatusBroken + } else { + inst.Errors = append(inst.Errors, fmt.Errorf("%s driver is running but host agent is not", inst.VMType)) + inst.Status = StatusBroken + } + } +} + // ReadPIDFile returns 0 if the PID file does not exist or the process has already terminated // (in which case the PID file will be removed). func ReadPIDFile(path string) (int, error) { @@ -190,6 +205,10 @@ func ReadPIDFile(path string) (int, error) { if err != nil { return 0, err } + // os.FindProcess will only return running processes on Windows, exit early + if runtime.GOOS == "windows" { + return pid, nil + } err = proc.Signal(syscall.Signal(0)) if err != nil { if errors.Is(err, os.ErrProcessDone) { @@ -328,7 +347,7 @@ func PrintInstances(w io.Writer, instances []*Instance, format string, options * fmt.Fprintf(w, "%s\t%s\t%s", instance.Name, instance.Status, - fmt.Sprintf("127.0.0.1:%d", instance.SSHLocalPort), + fmt.Sprintf("%s:%d", instance.SSHAddress, instance.SSHLocalPort), ) if !hideType { fmt.Fprintf(w, "\t%s", diff --git a/pkg/store/instance_test.go b/pkg/store/instance_test.go index 837ce1389b60..2b2088b71a61 100644 --- a/pkg/store/instance_test.go +++ b/pkg/store/instance_test.go @@ -19,11 +19,12 @@ var goarch = limayaml.NewArch(runtime.GOARCH) var space = strings.Repeat(" ", len(goarch)-4) var instance = Instance{ - Name: "foo", - Status: StatusStopped, - VMType: vmtype, - Arch: goarch, - Dir: "dir", + Name: "foo", + Status: StatusStopped, + VMType: vmtype, + Arch: goarch, + Dir: "dir", + SSHAddress: "127.0.0.1", } var table = "NAME STATUS SSH CPUS MEMORY DISK DIR\n" + diff --git a/pkg/store/instance_unix.go b/pkg/store/instance_unix.go new file mode 100644 index 000000000000..156116839154 --- /dev/null +++ b/pkg/store/instance_unix.go @@ -0,0 +1,14 @@ +//go:build !windows +// +build !windows + +package store + +import "github.com/lima-vm/lima/pkg/limayaml" + +func inspectStatus(instDir string, inst *Instance, y *limayaml.LimaYAML) { + inspectStatusWithPIDFiles(instDir, inst, y) +} + +func GetSSHAddress(_ string) (string, error) { + return "127.0.0.1", nil +} diff --git a/pkg/store/instance_windows.go b/pkg/store/instance_windows.go new file mode 100644 index 000000000000..dae0b4b34dd5 --- /dev/null +++ b/pkg/store/instance_windows.go @@ -0,0 +1,101 @@ +package store + +import ( + "fmt" + "os/exec" + "regexp" + "strings" + + "github.com/lima-vm/lima/pkg/executil" + "github.com/lima-vm/lima/pkg/limayaml" +) + +func inspectStatus(instDir string, inst *Instance, y *limayaml.LimaYAML) { + if inst.VMType == limayaml.WSL2 { + status, err := GetWslStatus(inst.Name) + if err != nil { + inst.Status = StatusBroken + inst.Errors = append(inst.Errors, err) + } else { + inst.Status = status + } + + inst.SSHLocalPort = 22 + + if inst.Status == StatusRunning { + sshAddr, err := getWslSSHAddress(inst.Name) + if err == nil { + inst.SSHAddress = sshAddr + } else { + inst.Errors = append(inst.Errors, err) + } + } + } else { + inspectStatusWithPIDFiles(instDir, inst, y) + } +} + +// GetWslStatus runs `wsl --list --verbose` and parses its output +// +// Expected output (whitespace preserved): +// PS > wsl --list --verbose +// +// NAME STATE VERSION +// +// * Ubuntu Stopped 2 +func GetWslStatus(instName string) (string, error) { + distroName := "lima-" + instName + out, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--list", + "--verbose", + }) + if err != nil { + return "", fmt.Errorf("failed to run `wsl --list --verbose`, err: %w", err) + } + + if len(out) == 0 { + return StatusBroken, fmt.Errorf("failed to read instance state for instance %s, try running `wsl --list --verbose` to debug, err: %w", instName, err) + } + + var instState string + // wsl --list --verbose may have differernt headers depending on localization, just split by line + for _, rows := range strings.Split(strings.ReplaceAll(string(out), "\r\n", "\n"), "\n") { + cols := regexp.MustCompile(`\s+`).Split(strings.TrimSpace(rows), -1) + nameIdx := 0 + // '*' indicates default instance + if cols[0] == "*" { + nameIdx = 1 + } + if cols[nameIdx] == distroName { + instState = cols[nameIdx+1] + break + } + } + + if instState == "" { + return StatusUninitialized, nil + } + + return instState, nil +} + +func GetSSHAddress(instName string) (string, error) { + return getWslSSHAddress(instName) +} + +// GetWslSSHAddress runs a hostname command to get the IP from inside of a wsl2 VM. +// +// Expected output (whitespace preserved, [] for optional): +// PS > wsl -d bash -c hostname -I | cut -d' ' -f1 +// 168.1.1.1 [10.0.0.1] +func getWslSSHAddress(instName string) (string, error) { + distroName := "lima-" + instName + cmd := exec.Command("wsl.exe", "-d", distroName, "bash", "-c", `hostname -I | cut -d ' ' -f1`) + out, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("failed to get hostname for instance %s, err: %w", instName, err) + } + + return strings.TrimSpace(string(out)), nil +} diff --git a/pkg/windows/process_windows.go b/pkg/windows/process_windows.go new file mode 100644 index 000000000000..566c02112d9b --- /dev/null +++ b/pkg/windows/process_windows.go @@ -0,0 +1,40 @@ +//go:build windows +// +build windows + +package windows + +import ( + "encoding/json" + "fmt" + "os/exec" +) + +type CommandLineJSON []struct { + CommandLine string +} + +// GetProcessCommandLine returns a slice of string containing all commandlines for a given process name. +func GetProcessCommandLine(name string) ([]string, error) { + out, err := exec.Command( + "powershell.exe", + "-nologo", + "-noprofile", + fmt.Sprintf( + `Get-CimInstance Win32_Process -Filter "name = '%s'" | Select CommandLine | ConvertTo-Json`, + name, + ), + ).CombinedOutput() + if err != nil { + return nil, err + } + + var outJSON CommandLineJSON + json.Unmarshal([]byte(out), &outJSON) + + var ret []string + for _, s := range outJSON { + ret = append(ret, s.CommandLine) + } + + return ret, nil +} diff --git a/pkg/windows/registry_windows.go b/pkg/windows/registry_windows.go new file mode 100644 index 000000000000..a5298bc9322b --- /dev/null +++ b/pkg/windows/registry_windows.go @@ -0,0 +1,225 @@ +//go:build windows +// +build windows + +package windows + +import ( + "fmt" + "math/rand" + "sort" + "strconv" + "strings" + + "golang.org/x/exp/slices" + "golang.org/x/sys/windows/registry" +) + +const ( + guestCommunicationsPrefix = `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Virtualization\GuestCommunicationServices` + MagicVSOCKSuffix = "-facb-11e6-bd58-64006a7986d3" + wslDistroInfoPrefix = `SOFTWARE\Microsoft\Windows\CurrentVersion\Lxss` +) + +// AddVSockRegistryKey makes a vsock server running on the host acceessible in guests. +func AddVSockRegistryKey(port int) error { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return err + } + defer rootKey.Close() + + used, err := getUsedPorts(rootKey) + if err != nil { + return err + } + + if slices.Contains(used, port) { + return fmt.Errorf("port %q in use", port) + } + + vsockKeyPath := fmt.Sprintf(`%x%s`, port, MagicVSOCKSuffix) + vSockKey, _, err := registry.CreateKey( + rootKey, + vsockKeyPath, + registry.ALL_ACCESS, + ) + if err != nil { + return fmt.Errorf( + "failed to create new key (%s%s): %w", + guestCommunicationsPrefix, + vsockKeyPath, + err, + ) + } + defer vSockKey.Close() + + return nil +} + +// RemoveVSockRegistryKey removes entries created by AddVSockRegistryKey. +func RemoveVSockRegistryKey(port int) error { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return err + } + defer rootKey.Close() + + vsockKeyPath := fmt.Sprintf(`%x%s`, port, MagicVSOCKSuffix) + if err := registry.DeleteKey(rootKey, vsockKeyPath); err != nil { + return fmt.Errorf( + "failed to create new key (%s%s): %w", + guestCommunicationsPrefix, + vsockKeyPath, + err, + ) + } + + return nil +} + +// IsVSockPortFree determines if a VSock port has been registiered already. +func IsVSockPortFree(port int) (bool, error) { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return false, err + } + defer rootKey.Close() + + used, err := getUsedPorts(rootKey) + if err != nil { + return false, err + } + + if slices.Contains(used, port) { + return false, nil + } + + return true, nil +} + +// GetDistroID returns a DistroId GUID corresponding to a Lima instance name. +func GetDistroID(name string) (string, error) { + rootKey, err := registry.OpenKey( + registry.CURRENT_USER, + wslDistroInfoPrefix, + registry.READ, + ) + if err != nil { + return "", fmt.Errorf( + "failed to open Lxss key (%s): %w", + wslDistroInfoPrefix, + err, + ) + } + defer rootKey.Close() + + keys, err := rootKey.ReadSubKeyNames(-1) + + if err != nil { + return "", fmt.Errorf("failed to read subkey names for %s: %w", wslDistroInfoPrefix, err) + } + + var out string + for _, k := range keys { + subKey, err := registry.OpenKey( + registry.CURRENT_USER, + fmt.Sprintf(`%s\%s`, wslDistroInfoPrefix, k), + registry.READ, + ) + if err != nil { + return "", fmt.Errorf("failed to read subkey %q for key %q: %w", k, wslDistroInfoPrefix, err) + } + dn, _, err := subKey.GetStringValue("DistributionName") + if err != nil { + return "", fmt.Errorf("failed to read 'DistributionName' value for subkey %q of %q: %w", k, wslDistroInfoPrefix, err) + } + if dn == name { + out = k + break + } + } + + if out == "" { + return "", fmt.Errorf("failed to find matching DistroID for %q", name) + } + + return out, nil +} + +// GetRandomFreeVSockPort gets a list of all registered VSock ports and returns a non-registered port. +func GetRandomFreeVSockPort(min, max int) (int, error) { + rootKey, err := getGuestCommunicationServicesKey() + if err != nil { + return 0, err + } + defer rootKey.Close() + + used, err := getUsedPorts(rootKey) + if err != nil { + return 0, err + } + + type pair struct{ v, offset int } + tree := make([]pair, 1, len(used)+1) + tree[0] = pair{0, min} + + sort.Ints(used) + for i, v := range used { + if tree[len(tree)-1].v+tree[len(tree)-1].offset == v { + tree[len(tree)-1].offset++ + } else { + tree = append(tree, pair{v - min - i, min + i + 1}) + } + } + + v := rand.Intn(max - min + 1 - len(used)) + + for len(tree) > 1 { + m := len(tree) / 2 + if v < tree[m].v { + tree = tree[:m] + } else { + tree = tree[m:] + } + } + + return tree[0].offset + v, nil +} + +func getGuestCommunicationServicesKey() (registry.Key, error) { + rootKey, err := registry.OpenKey( + registry.LOCAL_MACHINE, + guestCommunicationsPrefix, + registry.WRITE|registry.READ, + ) + if err != nil { + return 0, fmt.Errorf( + "failed to open GuestCommunicationServices key (%s): %w", + guestCommunicationsPrefix, + err, + ) + } + + return rootKey, nil +} + +func getUsedPorts(key registry.Key) ([]int, error) { + keys, err := key.ReadSubKeyNames(-1) + if err != nil { + return nil, fmt.Errorf("failed to read subkey names for %s: %w", guestCommunicationsPrefix, err) + } + + out := []int{} + for _, k := range keys { + split := strings.Split(k, MagicVSOCKSuffix) + if len(split) == 2 { + i, err := strconv.Atoi(split[0]) + if err != nil { + return nil, fmt.Errorf("failed convert %q to int: %w", split[0], err) + } + out = append(out, i) + } + } + + return out, nil +} diff --git a/pkg/windows/wsl_util_windows.go b/pkg/windows/wsl_util_windows.go new file mode 100644 index 000000000000..c15b40be42f9 --- /dev/null +++ b/pkg/windows/wsl_util_windows.go @@ -0,0 +1,44 @@ +//go:build windows +// +build windows + +package windows + +import ( + "fmt" + "regexp" + "strings" +) + +// GetInstanceVMID returns the VM ID of a running WSL instance. +func GetInstanceVMID(instanceName string) (string, error) { + distroID, err := GetDistroID(instanceName) + if err != nil { + return "", err + } + + cmdLines, err := GetProcessCommandLine("wslhost.exe") + if err != nil { + return "", err + } + + re := regexp.MustCompile(`--vm-id\s\{(?P.{36})\}`) + if err != nil { + return "", err + } + + vmID := "" + for _, cmdLine := range cmdLines { + if strings.Contains(cmdLine, distroID) { + if matches := re.FindStringSubmatch(cmdLine); matches != nil { + vmID = matches[re.SubexpIndex("vmID")] + break + } + } + } + + if vmID == "" { + return "", fmt.Errorf("failed to find VM ID for instance %q", instanceName) + } + + return vmID, nil +} diff --git a/pkg/wsl2/fs.go b/pkg/wsl2/fs.go new file mode 100644 index 000000000000..7500985e2b96 --- /dev/null +++ b/pkg/wsl2/fs.go @@ -0,0 +1,35 @@ +package wsl2 + +import ( + "errors" + "os" + "path/filepath" + + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/fileutils" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/sirupsen/logrus" +) + +// EnsureFs downloads the root fs. +func EnsureFs(driver *driver.BaseDriver) error { + baseDisk := filepath.Join(driver.Instance.Dir, filenames.BaseDisk) + if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) { + var ensuredBaseDisk bool + errs := make([]error, len(driver.Yaml.Images)) + for i, f := range driver.Yaml.Images { + if _, err := fileutils.DownloadFile(baseDisk, f.File, true, "the image", *driver.Yaml.Arch); err != nil { + errs[i] = err + continue + } + ensuredBaseDisk = true + break + } + if !ensuredBaseDisk { + return fileutils.Errors(errs) + } + } + logrus.Info("Download succeeded") + + return nil +} diff --git a/pkg/wsl2/lima-init.TEMPLATE.sh b/pkg/wsl2/lima-init.TEMPLATE.sh new file mode 100644 index 000000000000..0812eb63c2a9 --- /dev/null +++ b/pkg/wsl2/lima-init.TEMPLATE.sh @@ -0,0 +1,5 @@ +set -eu; \ +export LOG_FILE=/var/log/lima-init.log; \ +exec > >(tee \$LOG_FILE) 2>&1; \ +export LIMA_CIDATA_MNT=$(/usr/bin/wslpath '{{.CIDataPath}}'); \ +exec \$LIMA_CIDATA_MNT/boot.sh; diff --git a/pkg/wsl2/vm_windows.go b/pkg/wsl2/vm_windows.go new file mode 100755 index 000000000000..ff7adf8e24a4 --- /dev/null +++ b/pkg/wsl2/vm_windows.go @@ -0,0 +1,142 @@ +//go:build windows +// +build windows + +package wsl2 + +import ( + "context" + _ "embed" + "fmt" + "os/exec" + "path/filepath" + "strings" + + "github.com/lima-vm/lima/pkg/executil" + "github.com/lima-vm/lima/pkg/store" + "github.com/lima-vm/lima/pkg/store/filenames" + "github.com/lima-vm/lima/pkg/textutil" + "github.com/sirupsen/logrus" +) + +// startVM calls WSL to start a VM. +func startVM(ctx context.Context, distroName string) error { + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--distribution", + distroName, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} + +// initVM calls WSL to import a new VM specifically for Lima. +func initVM(ctx context.Context, instanceDir, distroName string) error { + baseDisk := filepath.Join(instanceDir, filenames.BaseDisk) + logrus.Infof("Importing distro from %q to %q", baseDisk, instanceDir) + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--import", + distroName, + instanceDir, + baseDisk, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} + +// stopVM calls WSL to stop a running VM. +func stopVM(ctx context.Context, distroName string) error { + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--terminate", + distroName, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} + +//go:embed lima-init.TEMPLATE.sh +var limaBoot string + +// provisionVM starts Lima's boot process inside an already imported VM. +func provisionVM(ctx context.Context, instanceDir, instanceName, distroName string, errCh *chan error) error { + ciDataPath := filepath.Join(instanceDir, filenames.CIDataISODir) + m := map[string]string{ + "CIDataPath": ciDataPath, + } + out, err := textutil.ExecuteTemplate(limaBoot, m) + if err != nil { + return fmt.Errorf("failed to construct wsl boot.sh script: %w", err) + } + outString := strings.Replace(string(out), `\r\n`, `\n`, -1) + + go func() { + cmd := exec.CommandContext( + ctx, + "wsl.exe", + "-d", + distroName, + "bash", + "-c", + outString, + ) + if _, err := cmd.CombinedOutput(); err != nil { + *errCh <- fmt.Errorf( + "error running wslCommand that executes boot.sh: %w, "+ + "check /var/log/lima-init.log for more details", err) + } + + for { + select { + case <-ctx.Done(): + logrus.Info("Context closed, stopping vm") + if status, err := store.GetWslStatus(instanceName); err == nil && + status == store.StatusRunning { + stopVM(ctx, distroName) + } + } + } + }() + + return err +} + +// keepAlive runs a background process which in order to keep the WSL2 VM running in the background after launch. +func keepAlive(ctx context.Context, distroName string, errCh *chan error) { + keepAliveCmd := exec.CommandContext( + ctx, + "wsl.exe", + "-d", + distroName, + "bash", + "-c", + "nohup sleep 2147483647d >/dev/null 2>&1", + ) + + go func() { + if err := keepAliveCmd.Run(); err != nil { + *errCh <- fmt.Errorf( + "error running wsl keepAlive command: %w", err) + } + }() +} + +// unregisterVM calls WSL to unregister a VM. +func unregisterVM(ctx context.Context, distroName string) error { + logrus.Info("Unregistering WSL2 VM") + _, err := executil.RunUTF16leCommand([]string{ + "wsl.exe", + "--unregister", + distroName, + }, executil.WithContext(&ctx)) + if err != nil { + return err + } + return nil +} diff --git a/pkg/wsl2/wsl_driver_others.go b/pkg/wsl2/wsl_driver_others.go new file mode 100644 index 000000000000..3d264714d1b8 --- /dev/null +++ b/pkg/wsl2/wsl_driver_others.go @@ -0,0 +1,41 @@ +//go:build !windows || no_wsl +// +build !windows no_wsl + +package wsl2 + +import ( + "context" + "errors" + + "github.com/lima-vm/lima/pkg/driver" +) + +var ErrUnsupported = errors.New("vm driver 'wsl2' requires Windows 10 build 19041 or later (Hint: try recompiling Lima if you are seeing this error on Windows 10+)") + +const Enabled = false + +type LimaWslDriver struct { + *driver.BaseDriver +} + +func New(driver *driver.BaseDriver) *LimaWslDriver { + return &LimaWslDriver{ + BaseDriver: driver, + } +} + +func (l *LimaWslDriver) Validate() error { + return ErrUnsupported +} + +func (l *LimaWslDriver) CreateDisk() error { + return ErrUnsupported +} + +func (l *LimaWslDriver) Start(_ context.Context) (chan error, error) { + return nil, ErrUnsupported +} + +func (l *LimaWslDriver) Stop(_ context.Context) error { + return ErrUnsupported +} diff --git a/pkg/wsl2/wsl_driver_windows.go b/pkg/wsl2/wsl_driver_windows.go new file mode 100644 index 000000000000..8983158bc4a0 --- /dev/null +++ b/pkg/wsl2/wsl_driver_windows.go @@ -0,0 +1,170 @@ +//go:build windows +// +build windows + +package wsl2 + +import ( + "context" + "fmt" + "regexp" + + "github.com/lima-vm/lima/pkg/driver" + "github.com/lima-vm/lima/pkg/limayaml" + "github.com/lima-vm/lima/pkg/reflectutil" + "github.com/lima-vm/lima/pkg/store" + "github.com/sirupsen/logrus" +) + +const Enabled = true + +type LimaWslDriver struct { + *driver.BaseDriver +} + +func New(driver *driver.BaseDriver) *LimaWslDriver { + return &LimaWslDriver{ + BaseDriver: driver, + } +} + +func (l *LimaWslDriver) Validate() error { + if *l.Yaml.MountType != limayaml.WSLMount { + return fmt.Errorf("field `mountType` must be %q for WSL2 driver, got %q", limayaml.WSLMount, *l.Yaml.MountType) + } + // TODO: revise this list for WSL2 + if unknown := reflectutil.UnknownNonEmptyFields(l.Yaml, "VMType", + "Arch", + "Images", + "CPUType", + "Disk", + "Mounts", + "MountType", + "SSH", + "Provision", + "Containerd", + "Probes", + "PortForwards", + "Message", + "Env", + "DNS", + "HostResolver", + "PropagateProxyEnv", + ); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: %+v", *l.Yaml.VMType, unknown) + } + + if !limayaml.IsNativeArch(*l.Yaml.Arch) { + return fmt.Errorf("unsupported arch: %q", *l.Yaml.Arch) + } + + for k, v := range l.Yaml.CPUType { + if v != "" { + logrus.Warnf("Ignoring: vmType %s: cpuType[%q]: %q", *l.Yaml.VMType, k, v) + } + } + + re, err := regexp.Compile(`.*tar\.*`) + if err != nil { + return fmt.Errorf("failed to compile file check regex: %w", err) + } + for i, image := range l.Yaml.Images { + if unknown := reflectutil.UnknownNonEmptyFields(image, "File"); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: images[%d]: %+v", *l.Yaml.VMType, i, unknown) + } + // TODO: real filetype checks + match := re.MatchString(image.Location) + if image.Arch == *l.Yaml.Arch && !match { + return fmt.Errorf("unsupported image type for vmType: %s, tarball root file system required: %q", *l.Yaml.VMType, image.Location) + } + } + + for i, mount := range l.Yaml.Mounts { + if unknown := reflectutil.UnknownNonEmptyFields(mount); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: mounts[%d]: %+v", *l.Yaml.VMType, i, unknown) + } + } + + for i, network := range l.Yaml.Networks { + if unknown := reflectutil.UnknownNonEmptyFields(network); len(unknown) > 0 { + logrus.Warnf("Ignoring: vmType %s: networks[%d]: %+v", *l.Yaml.VMType, i, unknown) + } + } + + audioDevice := *l.Yaml.Audio.Device + if audioDevice != "" { + logrus.Warnf("Ignoring: vmType %s: `audio.device`: %+v", *l.Yaml.VMType, audioDevice) + } + + return nil +} + +func (l *LimaWslDriver) Start(ctx context.Context) (chan error, error) { + logrus.Infof("Starting WSL VM") + status, err := store.GetWslStatus(l.Instance.Name) + if err != nil { + return nil, err + } + + distroName := "lima-" + l.Instance.Name + + if status == store.StatusUninitialized { + if err := EnsureFs(l.BaseDriver); err != nil { + return nil, err + } + if err := initVM(ctx, l.BaseDriver.Instance.Dir, distroName); err != nil { + return nil, err + } + } + + errCh := make(chan error) + + if err := startVM(ctx, distroName); err != nil { + return nil, err + } + + if err := provisionVM( + ctx, + l.BaseDriver.Instance.Dir, + l.BaseDriver.Instance.Name, + distroName, + &errCh, + ); err != nil { + return nil, err + } + + keepAlive(ctx, distroName, &errCh) + + return errCh, err +} + +// Requires WSLg, which requires specific version of WSL2 to be installed. +// TODO: Add check and add support for WSLg (instead of VNC) to hostagent. +func (l *LimaWslDriver) CanRunGUI() bool { + // return *l.Yaml.Video.Display == "wsl" + return false +} + +func (l *LimaWslDriver) RunGUI() error { + return fmt.Errorf("RunGUI is not support for the given driver '%s' and diplay '%s'", "wsl", *l.Yaml.Video.Display) +} + +func (l *LimaWslDriver) Stop(ctx context.Context) error { + logrus.Info("Shutting down WSL2 VM") + distroName := "lima-" + l.Instance.Name + return stopVM(ctx, distroName) +} + +func (l *LimaWslDriver) Unregister(ctx context.Context) error { + distroName := "lima-" + l.Instance.Name + status, err := store.GetWslStatus(l.Instance.Name) + if err != nil { + return err + } + switch status { + case store.StatusRunning, store.StatusStopped, store.StatusBroken, store.StatusInstalling: + return unregisterVM(ctx, distroName) + } + + logrus.Info("VM not registered, skipping unregistration") + return nil +}