Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

cmd: add support for restarting a running instance #3323

Merged
merged 1 commit into from
Apr 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/limactl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func newApp() *cobra.Command {
newUnprotectCommand(),
newTunnelCommand(),
newTemplateCommand(),
newRestartCommand(),
)
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
rootCmd.AddCommand(startAtLoginCommand())
Expand Down
52 changes: 52 additions & 0 deletions cmd/limactl/restart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package main

import (
"github.com/lima-vm/lima/pkg/instance"
"github.com/lima-vm/lima/pkg/store"
"github.com/spf13/cobra"
)

func newRestartCommand() *cobra.Command {
restartCmd := &cobra.Command{
Use: "restart INSTANCE",
Short: "Restart a running instance",
Args: WrapArgsError(cobra.MaximumNArgs(1)),
RunE: restartAction,
ValidArgsFunction: restartBashComplete,
GroupID: basicCommand,
}

restartCmd.Flags().BoolP("force", "f", false, "force stop and restart the instance")
return restartCmd
}

func restartAction(cmd *cobra.Command, args []string) error {
instName := DefaultInstanceName
if len(args) > 0 {
instName = args[0]
}

inst, err := store.Inspect(instName)
if err != nil {
return err
}

force, err := cmd.Flags().GetBool("force")
if err != nil {
return err
}

ctx := cmd.Context()
if force {
return instance.RestartForcibly(ctx, inst)
}

return instance.Restart(ctx, inst)
}

func restartBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return bashCompleteInstanceNames(cmd)
}
2 changes: 1 addition & 1 deletion cmd/limactl/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func stopAction(cmd *cobra.Command, args []string) error {
if force {
instance.StopForcibly(inst)
} else {
err = instance.StopGracefully(inst)
err = instance.StopGracefully(inst, false)
}
// TODO: should we also reconcile networks if graceful stop returned an error?
if err == nil {
Expand Down
45 changes: 45 additions & 0 deletions pkg/instance/restart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright The Lima Authors
// SPDX-License-Identifier: Apache-2.0

package instance

import (
"context"

networks "github.com/lima-vm/lima/pkg/networks/reconcile"
"github.com/lima-vm/lima/pkg/store"
"github.com/sirupsen/logrus"
)

const launchHostAgentForeground = false

func Restart(ctx context.Context, inst *store.Instance) error {
if err := StopGracefully(inst, true); err != nil {
return err
}

if err := networks.Reconcile(ctx, inst.Name); err != nil {
return err
}

if err := Start(ctx, inst, "", launchHostAgentForeground); err != nil {
return err
}

return nil
}

func RestartForcibly(ctx context.Context, inst *store.Instance) error {
logrus.Info("Restarting the instance forcibly")
StopForcibly(inst)

if err := networks.Reconcile(ctx, inst.Name); err != nil {
return err
}

if err := Start(ctx, inst, "", launchHostAgentForeground); err != nil {
return err
}

return nil
}
39 changes: 37 additions & 2 deletions pkg/instance/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ import (
"github.com/sirupsen/logrus"
)

func StopGracefully(inst *store.Instance) error {
func StopGracefully(inst *store.Instance, isRestart bool) error {
if inst.Status != store.StatusRunning {
if isRestart {
logrus.Warn("The instance is not running, continuing with the restart")
return nil
}
return fmt.Errorf("expected status %q, got %q (maybe use `limactl stop -f`?)", store.StatusRunning, inst.Status)
}

Expand All @@ -31,7 +35,13 @@ func StopGracefully(inst *store.Instance) error {
}

logrus.Info("Waiting for the host agent and the driver processes to shut down")
return waitForHostAgentTermination(context.TODO(), inst, begin)
err := waitForHostAgentTermination(context.TODO(), inst, begin)
if err != nil {
return err
}

logrus.Info("Waiting for the instance to shut down")
return waitForInstanceShutdown(context.TODO(), inst)
}

func waitForHostAgentTermination(ctx context.Context, inst *store.Instance, begin time.Time) error {
Expand Down Expand Up @@ -64,6 +74,31 @@ func waitForHostAgentTermination(ctx context.Context, inst *store.Instance, begi
return nil
}

func waitForInstanceShutdown(ctx context.Context, inst *store.Instance) error {
ctx2, cancel := context.WithTimeout(ctx, 3*time.Minute)
defer cancel()

ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()

for {
select {
case <-ticker.C:
updatedInst, err := store.Inspect(inst.Name)
if err != nil {
return errors.New("failed to inspect instance status: " + err.Error())
}

if updatedInst.Status == store.StatusStopped {
logrus.Infof("The instance %s has shut down", updatedInst.Name)
return nil
}
case <-ctx2.Done():
return errors.New("timed out waiting for instance to shut down after 3 minutes")
}
}
}

func StopForcibly(inst *store.Instance) {
if inst.DriverPID > 0 {
logrus.Infof("Sending SIGKILL to the %s driver process %d", inst.VMType, inst.DriverPID)
Expand Down
Loading