From 050572eff01b9310044cdcd49d932bc3f326bef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 18 Oct 2025 15:27:40 +0200 Subject: [PATCH 1/4] Add experimental memory command for balloons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make it possible to set target memory size, for an instance. This way it should be possible to return memory to the host. Signed-off-by: Anders F Björklund --- cmd/limactl/main.go | 1 + cmd/limactl/memory.go | 91 +++++++++++++++++++++++++++ pkg/driver/driver.go | 2 + pkg/driver/external/client/methods.go | 4 ++ pkg/driver/qemu/errors.go | 8 +++ pkg/driver/qemu/qemu_driver.go | 4 ++ pkg/driver/vz/vz_driver_darwin.go | 16 +++++ pkg/driver/wsl2/wsl_driver_windows.go | 4 ++ pkg/hostagent/api/client/client.go | 15 +++++ pkg/hostagent/api/server/server.go | 29 +++++++++ pkg/hostagent/hostagent.go | 4 ++ pkg/httpclientutil/httpclientutil.go | 16 +++++ pkg/memory/memory.go | 37 +++++++++++ pkg/registry/registry_test.go | 1 + 14 files changed, 232 insertions(+) create mode 100644 cmd/limactl/memory.go create mode 100644 pkg/driver/qemu/errors.go create mode 100644 pkg/memory/memory.go diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go index a91332da2bc..256cfd4545a 100644 --- a/cmd/limactl/main.go +++ b/cmd/limactl/main.go @@ -203,6 +203,7 @@ func newApp() *cobra.Command { newNetworkCommand(), newCloneCommand(), newRenameCommand(), + newMemoryCommand(), ) addPluginCommands(rootCmd) diff --git a/cmd/limactl/memory.go b/cmd/limactl/memory.go new file mode 100644 index 00000000000..0c725da64d9 --- /dev/null +++ b/cmd/limactl/memory.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "strconv" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/lima-vm/lima/v2/pkg/memory" + "github.com/lima-vm/lima/v2/pkg/store" +) + +func newMemoryCommand() *cobra.Command { + memoryCmd := &cobra.Command{ + Use: "memory", + Short: "Manage instance memory", + PersistentPreRun: func(*cobra.Command, []string) { + logrus.Warn("`limactl memory` is experimental") + }, + GroupID: advancedCommand, + } + memoryCmd.AddCommand(newMemoryGetCommand()) + memoryCmd.AddCommand(newMemorySetCommand()) + + return memoryCmd +} + +func newMemoryGetCommand() *cobra.Command { + getCmd := &cobra.Command{ + Use: "get INSTANCE", + Short: "Get current memory", + Long: "Get the currently used total memory of an instance, in MiB", + Args: cobra.MinimumNArgs(1), + RunE: memoryGetAction, + ValidArgsFunction: memoryBashComplete, + } + + return getCmd +} + +func memoryGetAction(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + instName := args[0] + + inst, err := store.Inspect(ctx, instName) + if err != nil { + return err + } + + _ = inst + mem := 0 // TODO: implement + fmt.Fprintf(cmd.OutOrStdout(), "%d\n", mem>>20) + return nil +} + +func newMemorySetCommand() *cobra.Command { + setCmd := &cobra.Command{ + Use: "set INSTANCE memory AMOUNT", + Short: "Set target memory", + Long: "Set the target total memory of an instance, in MiB", + Args: cobra.MinimumNArgs(2), + RunE: memorySetAction, + ValidArgsFunction: memoryBashComplete, + } + + return setCmd +} + +func memorySetAction(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + instName := args[0] + meg, err := strconv.Atoi(args[1]) + if err != nil { + return err + } + + inst, err := store.Inspect(ctx, instName) + if err != nil { + return err + } + + return memory.SetTarget(ctx, inst, int64(meg)<<20) +} + +func memoryBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return bashCompleteInstanceNames(cmd) +} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 1c372f7dd7f..080e34ffc4f 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -88,6 +88,8 @@ type Driver interface { FillConfig(ctx context.Context, cfg *limatype.LimaYAML, filePath string) error SSHAddress(ctx context.Context) (string, error) + + SetTargetMemory(memory int64) error } type ConfiguredDriver struct { diff --git a/pkg/driver/external/client/methods.go b/pkg/driver/external/client/methods.go index b26114bed14..ab8860055ad 100644 --- a/pkg/driver/external/client/methods.go +++ b/pkg/driver/external/client/methods.go @@ -323,3 +323,7 @@ func (d *DriverClient) BootScripts() (map[string][]byte, error) { d.logger.Debugf("Boot scripts retrieved successfully: %d scripts", len(resp.Scripts)) return resp.Scripts, nil } + +func (d *DriverClient) SetTargetMemory(_ int64) error { + return errors.New("unavailable") +} diff --git a/pkg/driver/qemu/errors.go b/pkg/driver/qemu/errors.go new file mode 100644 index 00000000000..6ef698b5959 --- /dev/null +++ b/pkg/driver/qemu/errors.go @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package qemu + +import "errors" + +var errUnimplemented = errors.New("unimplemented") diff --git a/pkg/driver/qemu/qemu_driver.go b/pkg/driver/qemu/qemu_driver.go index 339bca2c9b2..7bc89e7f729 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -720,3 +720,7 @@ func (l *LimaQemuDriver) ForwardGuestAgent() bool { // if driver is not providing, use host agent return l.vSockPort == 0 && l.virtioPort == "" } + +func (l *LimaQemuDriver) SetTargetMemory(_ int64) error { + return errUnimplemented +} diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index 927e9b90d26..aad013a390d 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -437,3 +437,19 @@ func (l *LimaVzDriver) ForwardGuestAgent() bool { // If driver is not providing, use host agent return l.vSockPort == 0 && l.virtioPort == "" } + +func (l *LimaVzDriver) SetTargetMemory(memory int64) error { + if l.machine == nil { + return errors.New("no machine") + } + balloons := l.machine.MemoryBalloonDevices() + if len(balloons) != 1 { + return fmt.Errorf("unexpected number of devices: %d", len(balloons)) + } + balloon := vz.AsVirtioTraditionalMemoryBalloonDevice(balloons[0]) + if balloon == nil { + return errors.New("unexpected type of balloon") + } + balloon.SetTargetVirtualMachineMemorySize(uint64(memory)) + return nil +} diff --git a/pkg/driver/wsl2/wsl_driver_windows.go b/pkg/driver/wsl2/wsl_driver_windows.go index 4468dbf554e..d5f2d2ebd0d 100644 --- a/pkg/driver/wsl2/wsl_driver_windows.go +++ b/pkg/driver/wsl2/wsl_driver_windows.go @@ -357,3 +357,7 @@ func (l *LimaWslDriver) ForwardGuestAgent() bool { // If driver is not providing, use host agent return l.vSockPort == 0 && l.virtioPort == "" } + +func (l *LimaWslDriver) SetTargetMemory(_ int64) error { + return errUnimplemented +} diff --git a/pkg/hostagent/api/client/client.go b/pkg/hostagent/api/client/client.go index a71d25a99b3..504d2438045 100644 --- a/pkg/hostagent/api/client/client.go +++ b/pkg/hostagent/api/client/client.go @@ -11,6 +11,8 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" + "strings" "github.com/lima-vm/lima/v2/pkg/hostagent/api" "github.com/lima-vm/lima/v2/pkg/httpclientutil" @@ -19,6 +21,7 @@ import ( type HostAgentClient interface { HTTPClient() *http.Client Info(context.Context) (*api.Info, error) + SetTargetMemory(context.Context, int64) error } // NewHostAgentClient creates a client. @@ -65,3 +68,15 @@ func (c *client) Info(ctx context.Context) (*api.Info, error) { } return &info, nil } + +func (c *client) SetTargetMemory(ctx context.Context, memory int64) error { + u := fmt.Sprintf("http://%s/%s/memory", c.dummyHost, c.version) + body := strconv.FormatInt(memory, 10) + b := strings.NewReader(body) + resp, err := httpclientutil.Put(ctx, c.HTTPClient(), u, b) + if err != nil { + return err + } + defer resp.Body.Close() + return nil +} diff --git a/pkg/hostagent/api/server/server.go b/pkg/hostagent/api/server/server.go index 26da65976a6..41ad5e5a28b 100644 --- a/pkg/hostagent/api/server/server.go +++ b/pkg/hostagent/api/server/server.go @@ -6,7 +6,9 @@ package server import ( "context" "encoding/json" + "io" "net/http" + "strconv" "github.com/lima-vm/lima/v2/pkg/hostagent" "github.com/lima-vm/lima/v2/pkg/httputil" @@ -53,6 +55,33 @@ func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(m) } +// SetMemory is the handler for PUT /v1/memory. +func (b *Backend) SetMemory(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + body, err := io.ReadAll(r.Body) + if err != nil { + b.onError(w, err, http.StatusInternalServerError) + return + } + memory, err := strconv.ParseInt(string(body), 10, 64) + if err != nil { + b.onError(w, err, http.StatusInternalServerError) + return + } + + err = b.Agent.SetTargetMemory(memory) + if err != nil { + b.onError(w, err, http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} + func AddRoutes(r *http.ServeMux, b *Backend) { r.Handle("/v1/info", http.HandlerFunc(b.GetInfo)) + r.Handle("/v1/memory", http.HandlerFunc(b.SetMemory)) } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index d66f2812a91..aaaafcf1918 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -445,6 +445,10 @@ func (a *HostAgent) Run(ctx context.Context) error { return a.startRoutinesAndWait(ctx, errCh) } +func (a *HostAgent) SetTargetMemory(memory int64) error { + return a.driver.SetTargetMemory(memory) +} + func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error) error { stBase := events.Status{ SSHLocalPort: a.sshLocalPort, diff --git a/pkg/httpclientutil/httpclientutil.go b/pkg/httpclientutil/httpclientutil.go index c98f9281118..5fda99b0f12 100644 --- a/pkg/httpclientutil/httpclientutil.go +++ b/pkg/httpclientutil/httpclientutil.go @@ -67,6 +67,22 @@ func Post(ctx context.Context, c *http.Client, url string, body io.Reader) (*htt return resp, nil } +func Put(ctx context.Context, c *http.Client, url string, body io.Reader) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, "PUT", url, body) + if err != nil { + return nil, err + } + resp, err := c.Do(req) + if err != nil { + return nil, err + } + if err := Successful(resp); err != nil { + resp.Body.Close() + return nil, err + } + return resp, nil +} + func readAtMost(r io.Reader, maxBytes int) ([]byte, error) { lr := &io.LimitedReader{ R: r, diff --git a/pkg/memory/memory.go b/pkg/memory/memory.go new file mode 100644 index 00000000000..b77ef8bcc30 --- /dev/null +++ b/pkg/memory/memory.go @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package memory + +import ( + "context" + "path/filepath" + "time" + + hostagentclient "github.com/lima-vm/lima/v2/pkg/hostagent/api/client" + "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/store" +) + +func SetTarget(ctx context.Context, inst *limatype.Instance, memory int64) error { + hostAgentPID, err := store.ReadPIDFile(filepath.Join(inst.Dir, filenames.HostAgentPID)) + if err != nil { + return err + } + if hostAgentPID != 0 { + haSock := filepath.Join(inst.Dir, filenames.HostAgentSock) + haClient, err := hostagentclient.NewHostAgentClient(haSock) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + err = haClient.SetTargetMemory(ctx, memory) + if err != nil { + return err + } + } + + return nil +} diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 8e783f44994..204a7abcf4a 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -51,6 +51,7 @@ func (m *mockDriver) Configure(_ *limatype.Instance) *driver.ConfiguredDriver func (m *mockDriver) FillConfig(_ context.Context, _ *limatype.LimaYAML, _ string) error { return nil } func (m *mockDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string { return "" } func (m *mockDriver) SSHAddress(_ context.Context) (string, error) { return "", nil } +func (m *mockDriver) SetTargetMemory(int64) error { return nil } func (m *mockDriver) BootScripts() (map[string][]byte, error) { return nil, nil } func TestRegister(t *testing.T) { From cd3bee510ca00c4610b5deb2fea8376df8ea9e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 18 Oct 2025 17:17:39 +0200 Subject: [PATCH 2/4] Remove finalizer on balloon to avoid segfault MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/driver/vz/vz_driver_darwin.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index aad013a390d..7e5c741d79c 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -451,5 +451,7 @@ func (l *LimaVzDriver) SetTargetMemory(memory int64) error { return errors.New("unexpected type of balloon") } balloon.SetTargetVirtualMachineMemorySize(uint64(memory)) + // avoid segfault, when trying to Release + runtime.SetFinalizer(balloon, nil) return nil } From ab7ccd448d50613664641b996b1c901b71ce9271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sat, 18 Oct 2025 17:58:06 +0200 Subject: [PATCH 3/4] Add virtio balloon support to the qemu driver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- pkg/driver/qemu/errors.go | 8 -------- pkg/driver/qemu/qemu.go | 2 ++ pkg/driver/qemu/qemu_driver.go | 20 ++++++++++++++++++-- 3 files changed, 20 insertions(+), 10 deletions(-) delete mode 100644 pkg/driver/qemu/errors.go diff --git a/pkg/driver/qemu/errors.go b/pkg/driver/qemu/errors.go deleted file mode 100644 index 6ef698b5959..00000000000 --- a/pkg/driver/qemu/errors.go +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: Copyright The Lima Authors -// SPDX-License-Identifier: Apache-2.0 - -package qemu - -import "errors" - -var errUnimplemented = errors.New("unimplemented") diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go index ee7456d7b2c..7236766564f 100644 --- a/pkg/driver/qemu/qemu.go +++ b/pkg/driver/qemu/qemu.go @@ -835,6 +835,8 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er // virtio-rng-pci accelerates starting up the OS, according to https://wiki.gentoo.org/wiki/QEMU/Options args = append(args, "-device", "virtio-rng-pci") + args = append(args, "-device", "virtio-balloon") + // Input input := "mouse" diff --git a/pkg/driver/qemu/qemu_driver.go b/pkg/driver/qemu/qemu_driver.go index 7bc89e7f729..fb5dce985ac 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -25,6 +25,7 @@ import ( "github.com/coreos/go-semver/semver" "github.com/digitalocean/go-qemu/qmp" "github.com/digitalocean/go-qemu/qmp/raw" + "github.com/docker/go-units" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/driver" @@ -721,6 +722,21 @@ func (l *LimaQemuDriver) ForwardGuestAgent() bool { return l.vSockPort == 0 && l.virtioPort == "" } -func (l *LimaQemuDriver) SetTargetMemory(_ int64) error { - return errUnimplemented +func (l *LimaQemuDriver) SetTargetMemory(memory int64) error { + qmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock) + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return err + } + if err := qmpClient.Connect(); err != nil { + return err + } + defer func() { _ = qmpClient.Disconnect() }() + rawClient := raw.NewMonitor(qmpClient) + logrus.Infof("Balloon target size: %s", units.BytesSize(float64(memory))) + err = rawClient.Balloon(memory) + if err != nil { + return err + } + return nil } From 125981f5f45cfb5e0ba3995fafe55b3409eafc99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20F=20Bj=C3=B6rklund?= Date: Sun, 19 Oct 2025 09:32:14 +0200 Subject: [PATCH 4/4] Add command to get the current memory usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Anders F Björklund --- cmd/limactl/memory.go | 6 ++- pkg/driver/driver.go | 2 + pkg/driver/external/client/methods.go | 4 ++ pkg/driver/qemu/qemu_driver.go | 19 ++++++++ pkg/driver/vz/vz_driver_darwin.go | 18 +++++++ pkg/driver/wsl2/wsl_driver_windows.go | 4 ++ pkg/hostagent/api/client/client.go | 20 ++++++++ pkg/hostagent/api/server/server.go | 22 ++++++++- pkg/hostagent/hostagent.go | 4 ++ pkg/memory/memory.go | 67 +++++++++++++++++++++++++++ pkg/registry/registry_test.go | 1 + 11 files changed, 164 insertions(+), 3 deletions(-) diff --git a/cmd/limactl/memory.go b/cmd/limactl/memory.go index 0c725da64d9..30029d0962e 100644 --- a/cmd/limactl/memory.go +++ b/cmd/limactl/memory.go @@ -51,8 +51,10 @@ func memoryGetAction(cmd *cobra.Command, args []string) error { return err } - _ = inst - mem := 0 // TODO: implement + mem, err := memory.GetCurrent(ctx, inst) + if err != nil { + return err + } fmt.Fprintf(cmd.OutOrStdout(), "%d\n", mem>>20) return nil } diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go index 080e34ffc4f..4f819e84463 100644 --- a/pkg/driver/driver.go +++ b/pkg/driver/driver.go @@ -89,6 +89,8 @@ type Driver interface { SSHAddress(ctx context.Context) (string, error) + GetCurrentMemory() (int64, error) + SetTargetMemory(memory int64) error } diff --git a/pkg/driver/external/client/methods.go b/pkg/driver/external/client/methods.go index ab8860055ad..a2504aba21c 100644 --- a/pkg/driver/external/client/methods.go +++ b/pkg/driver/external/client/methods.go @@ -324,6 +324,10 @@ func (d *DriverClient) BootScripts() (map[string][]byte, error) { return resp.Scripts, nil } +func (d *DriverClient) GetCurrentMemory() (int64, error) { + return 0, errors.New("unavailable") +} + func (d *DriverClient) SetTargetMemory(_ int64) error { return errors.New("unavailable") } diff --git a/pkg/driver/qemu/qemu_driver.go b/pkg/driver/qemu/qemu_driver.go index fb5dce985ac..88f1de847fe 100644 --- a/pkg/driver/qemu/qemu_driver.go +++ b/pkg/driver/qemu/qemu_driver.go @@ -722,6 +722,25 @@ func (l *LimaQemuDriver) ForwardGuestAgent() bool { return l.vSockPort == 0 && l.virtioPort == "" } +func (l *LimaQemuDriver) GetCurrentMemory() (int64, error) { + qmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock) + qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) + if err != nil { + return 0, err + } + if err := qmpClient.Connect(); err != nil { + return 0, err + } + defer func() { _ = qmpClient.Disconnect() }() + rawClient := raw.NewMonitor(qmpClient) + info, err := rawClient.QueryBalloon() + if err != nil { + return 0, err + } + logrus.Infof("Balloon actual size: %s", units.BytesSize(float64(info.Actual))) + return info.Actual, nil +} + func (l *LimaQemuDriver) SetTargetMemory(memory int64) error { qmpSockPath := filepath.Join(l.Instance.Dir, filenames.QMPSock) qmpClient, err := qmp.NewSocketMonitor("unix", qmpSockPath, 5*time.Second) diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index 7e5c741d79c..6e37fb3dd92 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -438,6 +438,24 @@ func (l *LimaVzDriver) ForwardGuestAgent() bool { return l.vSockPort == 0 && l.virtioPort == "" } +func (l *LimaVzDriver) GetCurrentMemory() (int64, error) { + if l.machine == nil { + return 0, errors.New("no machine") + } + balloons := l.machine.MemoryBalloonDevices() + if len(balloons) != 1 { + return 0, fmt.Errorf("unexpected number of devices: %d", len(balloons)) + } + balloon := vz.AsVirtioTraditionalMemoryBalloonDevice(balloons[0]) + if balloon == nil { + return 0, errors.New("unexpected type of balloon") + } + // avoid segfault, when trying to Release + runtime.SetFinalizer(balloon, nil) + memory := balloon.GetTargetVirtualMachineMemorySize() + return int64(memory), nil +} + func (l *LimaVzDriver) SetTargetMemory(memory int64) error { if l.machine == nil { return errors.New("no machine") diff --git a/pkg/driver/wsl2/wsl_driver_windows.go b/pkg/driver/wsl2/wsl_driver_windows.go index d5f2d2ebd0d..a1a4d6254f9 100644 --- a/pkg/driver/wsl2/wsl_driver_windows.go +++ b/pkg/driver/wsl2/wsl_driver_windows.go @@ -358,6 +358,10 @@ func (l *LimaWslDriver) ForwardGuestAgent() bool { return l.vSockPort == 0 && l.virtioPort == "" } +func (l *LimaWslDriver) GetCurrentMemory() (int64, error) { + return 0, errUnimplemented +} + func (l *LimaWslDriver) SetTargetMemory(_ int64) error { return errUnimplemented } diff --git a/pkg/hostagent/api/client/client.go b/pkg/hostagent/api/client/client.go index 504d2438045..33b96789561 100644 --- a/pkg/hostagent/api/client/client.go +++ b/pkg/hostagent/api/client/client.go @@ -10,6 +10,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "strconv" "strings" @@ -21,6 +22,7 @@ import ( type HostAgentClient interface { HTTPClient() *http.Client Info(context.Context) (*api.Info, error) + GetCurrentMemory(context.Context) (int64, error) SetTargetMemory(context.Context, int64) error } @@ -69,6 +71,24 @@ func (c *client) Info(ctx context.Context) (*api.Info, error) { return &info, nil } +func (c *client) GetCurrentMemory(ctx context.Context) (int64, error) { + u := fmt.Sprintf("http://%s/%s/memory", c.dummyHost, c.version) + resp, err := httpclientutil.Get(ctx, c.HTTPClient(), u) + if err != nil { + return 0, err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, err + } + memory, err := strconv.ParseInt(string(body), 10, 64) + if err != nil { + return 0, err + } + return memory, nil +} + func (c *client) SetTargetMemory(ctx context.Context, memory int64) error { u := fmt.Sprintf("http://%s/%s/memory", c.dummyHost, c.version) body := strconv.FormatInt(memory, 10) diff --git a/pkg/hostagent/api/server/server.go b/pkg/hostagent/api/server/server.go index 41ad5e5a28b..0cb4764a048 100644 --- a/pkg/hostagent/api/server/server.go +++ b/pkg/hostagent/api/server/server.go @@ -55,6 +55,25 @@ func (b *Backend) GetInfo(w http.ResponseWriter, r *http.Request) { _, _ = w.Write(m) } +// GetMemory is the handler for GET /v1/memory. +func (b *Backend) GetMemory(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + memory, err := b.Agent.GetCurrentMemory() + if err != nil { + b.onError(w, err, http.StatusInternalServerError) + return + } + s := strconv.FormatInt(memory, 10) + + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(s)) +} + // SetMemory is the handler for PUT /v1/memory. func (b *Backend) SetMemory(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPut { @@ -83,5 +102,6 @@ func (b *Backend) SetMemory(w http.ResponseWriter, r *http.Request) { func AddRoutes(r *http.ServeMux, b *Backend) { r.Handle("/v1/info", http.HandlerFunc(b.GetInfo)) - r.Handle("/v1/memory", http.HandlerFunc(b.SetMemory)) + r.Handle("GET /v1/memory", http.HandlerFunc(b.GetMemory)) + r.Handle("PUT /v1/memory", http.HandlerFunc(b.SetMemory)) } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index aaaafcf1918..88d602613d3 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -445,6 +445,10 @@ func (a *HostAgent) Run(ctx context.Context) error { return a.startRoutinesAndWait(ctx, errCh) } +func (a *HostAgent) GetCurrentMemory() (int64, error) { + return a.driver.GetCurrentMemory() +} + func (a *HostAgent) SetTargetMemory(memory int64) error { return a.driver.SetTargetMemory(memory) } diff --git a/pkg/memory/memory.go b/pkg/memory/memory.go index b77ef8bcc30..6376aa8eb11 100644 --- a/pkg/memory/memory.go +++ b/pkg/memory/memory.go @@ -5,15 +5,82 @@ package memory import ( "context" + "fmt" + "os/exec" "path/filepath" + "strconv" + "strings" "time" hostagentclient "github.com/lima-vm/lima/v2/pkg/hostagent/api/client" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/sshutil" "github.com/lima-vm/lima/v2/pkg/store" ) +func GetCurrent(ctx context.Context, inst *limatype.Instance) (int64, error) { + var memory int64 + hostAgentPID, err := store.ReadPIDFile(filepath.Join(inst.Dir, filenames.HostAgentPID)) + if err != nil { + return 0, err + } + if hostAgentPID != 0 { + haSock := filepath.Join(inst.Dir, filenames.HostAgentSock) + haClient, err := hostagentclient.NewHostAgentClient(haSock) + if err != nil { + return 0, err + } + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + memory, err = haClient.GetCurrentMemory(ctx) + if err != nil { + return 0, err + } + } + + sshExe, err := sshutil.NewSSHExe() + if err != nil { + return 0, err + } + sshOpts, err := sshutil.CommonOpts(ctx, sshExe, false) + if err != nil { + return 0, err + } + sshArgs := append(sshutil.SSHArgsFromOpts(sshOpts), + "-p", fmt.Sprintf("%d", inst.SSHLocalPort), + fmt.Sprintf("%s@%s", *inst.Config.User.Name, inst.SSHAddress), + ) + + args := []string{"cat", "/proc/meminfo"} + sshCmd := exec.CommandContext(ctx, sshExe.Exe, append(sshArgs, args...)...) + out, err := sshCmd.Output() + if err != nil { + return 0, err + } + + var available int64 + for _, line := range strings.Split(string(out), "\n") { + if !strings.HasPrefix(line, "MemAvailable: ") { + continue + } + fields := strings.Fields(line) + if len(fields) < 3 { + return 0, fmt.Errorf("unexpected line: %s", line) + } + num, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + return 0, err + } + if fields[2] == "kB" { + num *= 1024 + } + available = num + } + + return memory - available, nil +} + func SetTarget(ctx context.Context, inst *limatype.Instance, memory int64) error { hostAgentPID, err := store.ReadPIDFile(filepath.Join(inst.Dir, filenames.HostAgentPID)) if err != nil { diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 204a7abcf4a..b2d54cbf906 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -51,6 +51,7 @@ func (m *mockDriver) Configure(_ *limatype.Instance) *driver.ConfiguredDriver func (m *mockDriver) FillConfig(_ context.Context, _ *limatype.LimaYAML, _ string) error { return nil } func (m *mockDriver) InspectStatus(_ context.Context, _ *limatype.Instance) string { return "" } func (m *mockDriver) SSHAddress(_ context.Context) (string, error) { return "", nil } +func (m *mockDriver) GetCurrentMemory() (int64, error) { return 0, nil } func (m *mockDriver) SetTargetMemory(int64) error { return nil } func (m *mockDriver) BootScripts() (map[string][]byte, error) { return nil, nil }