diff --git a/cmd/limactl/delete.go b/cmd/limactl/delete.go index 168f41d47ef..881dff680bb 100644 --- a/cmd/limactl/delete.go +++ b/cmd/limactl/delete.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "os" - "runtime" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -50,13 +49,16 @@ func deleteAction(cmd *cobra.Command, args []string) error { if err := instance.Delete(cmd.Context(), inst, force); err != nil { return fmt.Errorf("failed to delete instance %q: %w", instName, err) } - if runtime.GOOS == "darwin" || runtime.GOOS == "linux" { - deleted, err := autostart.DeleteStartAtLoginEntry(ctx, runtime.GOOS, instName) - if err != nil && !errors.Is(err, os.ErrNotExist) { - logrus.WithError(err).Warnf("The autostart file for instance %q does not exist", instName) - } else if deleted { - logrus.Infof("The autostart file %q has been deleted", autostart.GetFilePath(runtime.GOOS, instName)) + if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + logrus.WithError(err).Warnf("Failed to check if the autostart entry for instance %q is registered", instName) + } else if registered { + if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil { + logrus.WithError(err).Warnf("Failed to unregister the autostart entry for instance %q", instName) + } else { + logrus.Infof("The autostart entry for instance %q has been unregistered", instName) } + } else { + logrus.Infof("The autostart entry for instance %q is not registered", instName) } logrus.Infof("Deleted %q (%q)", instName, inst.Dir) } diff --git a/cmd/limactl/edit.go b/cmd/limactl/edit.go index 80e263a2637..bb30c0d7dd4 100644 --- a/cmd/limactl/edit.go +++ b/cmd/limactl/edit.go @@ -14,6 +14,7 @@ import ( "github.com/spf13/cobra" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" + "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/editutil" "github.com/lima-vm/lima/v2/pkg/instance" @@ -155,9 +156,14 @@ func editAction(cmd *cobra.Command, args []string) error { if !startNow { return nil } - err = networks.Reconcile(ctx, inst.Name) - if err != nil { - return err + // Network reconciliation will be performed by the process launched by the autostart manager + if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if !registered { + err = networks.Reconcile(ctx, inst.Name) + if err != nil { + return err + } } // store.Inspect() syncs values between inst.YAML and the store. diff --git a/cmd/limactl/shell.go b/cmd/limactl/shell.go index d769308527d..ba7d696217e 100644 --- a/cmd/limactl/shell.go +++ b/cmd/limactl/shell.go @@ -20,6 +20,7 @@ import ( "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/envutil" "github.com/lima-vm/lima/v2/pkg/instance" "github.com/lima-vm/lima/v2/pkg/ioutilx" @@ -101,9 +102,14 @@ func shellAction(cmd *cobra.Command, args []string) error { return nil } - err = networks.Reconcile(ctx, inst.Name) - if err != nil { - return err + // Network reconciliation will be performed by the process launched by the autostart manager + if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if !registered { + err = networks.Reconcile(ctx, inst.Name) + if err != nil { + return err + } } err = instance.Start(ctx, inst, false, false) diff --git a/cmd/limactl/start-at-login_unix.go b/cmd/limactl/start-at-login_unix.go index ad3e94d62ea..f3541fef5e9 100644 --- a/cmd/limactl/start-at-login_unix.go +++ b/cmd/limactl/start-at-login_unix.go @@ -7,8 +7,8 @@ package main import ( "errors" + "fmt" "os" - "runtime" "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -38,18 +38,24 @@ func startAtLoginAction(cmd *cobra.Command, args []string) error { if err != nil { return err } - if startAtLogin { - if err := autostart.CreateStartAtLoginEntry(ctx, runtime.GOOS, inst.Name, inst.Dir); err != nil { - logrus.WithError(err).Warnf("Can't create an autostart file for instance %q", inst.Name) - } else { - logrus.Infof("The autostart file %q has been created or updated", autostart.GetFilePath(runtime.GOOS, inst.Name)) + if registered, err := autostart.IsRegistered(ctx, inst); err != nil { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if startAtLogin { + verb := "create" + if registered { + verb = "update" + } + if err := autostart.RegisterToStartAtLogin(ctx, inst); err != nil { + return fmt.Errorf("failed to %s the autostart entry for instance %q: %w", verb, inst.Name, err) } + logrus.Infof("The autostart entry for instance %q has been %sd", inst.Name, verb) } else { - deleted, err := autostart.DeleteStartAtLoginEntry(ctx, runtime.GOOS, instName) - if err != nil { - logrus.WithError(err).Warnf("The autostart file %q could not be deleted", instName) - } else if deleted { - logrus.Infof("The autostart file %q has been deleted", autostart.GetFilePath(runtime.GOOS, instName)) + if !registered { + logrus.Infof("The autostart entry for instance %q is not registered", inst.Name) + } else if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil { + return fmt.Errorf("failed to unregister the autostart entry for instance %q: %w", inst.Name, err) + } else { + logrus.Infof("The autostart entry for instance %q has been unregistered", inst.Name) } } diff --git a/cmd/limactl/start.go b/cmd/limactl/start.go index 24a041fde55..8af49439176 100644 --- a/cmd/limactl/start.go +++ b/cmd/limactl/start.go @@ -17,6 +17,7 @@ import ( "github.com/spf13/pflag" "github.com/lima-vm/lima/v2/cmd/limactl/editflags" + "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/driverutil" "github.com/lima-vm/lima/v2/pkg/editutil" "github.com/lima-vm/lima/v2/pkg/instance" @@ -580,9 +581,14 @@ func startAction(cmd *cobra.Command, args []string) error { logrus.Warnf("expected status %q, got %q", limatype.StatusStopped, inst.Status) } ctx := cmd.Context() - err = networks.Reconcile(ctx, inst.Name) - if err != nil { - return err + // Network reconciliation will be performed by the process launched by the autostart manager + if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if !registered { + err = networks.Reconcile(ctx, inst.Name) + if err != nil { + return err + } } launchHostAgentForeground := false diff --git a/go.mod b/go.mod index e5cf20e18bd..75041557a9f 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/containerd/continuity v0.4.5 github.com/containers/gvisor-tap-vsock v0.8.7 // gomodjail:unconfined github.com/coreos/go-semver v0.3.1 + github.com/coreos/go-systemd/v22 v22.6.0 github.com/cpuguy83/go-md2man/v2 v2.0.7 github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7 github.com/diskfs/go-diskfs v1.7.0 // gomodjail:unconfined diff --git a/go.sum b/go.sum index fc300d40470..aedec2d5f0d 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/containers/gvisor-tap-vsock v0.8.7 h1:mFMMU5CIXO9sbtsgECc90loUHx15km3 github.com/containers/gvisor-tap-vsock v0.8.7/go.mod h1:Rf2gm4Lpac0IZbg8wwQDh7UuKCxHmnxar0hEZ08OXY8= github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= diff --git a/pkg/autostart/autostart.go b/pkg/autostart/autostart.go index c2b15a82790..3558032c592 100644 --- a/pkg/autostart/autostart.go +++ b/pkg/autostart/autostart.go @@ -6,118 +6,68 @@ package autostart import ( "context" - _ "embed" - "errors" - "fmt" - "os" - "os/exec" - "path" - "path/filepath" - "strconv" - "strings" + "runtime" + "sync" - "github.com/lima-vm/lima/v2/pkg/textutil" + "github.com/lima-vm/lima/v2/pkg/autostart/systemd" + "github.com/lima-vm/lima/v2/pkg/limatype" ) -//go:embed lima-vm@INSTANCE.service -var systemdTemplate string +// IsRegistered checks if the instance is registered to start at login. +func IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error) { + return manager().IsRegistered(ctx, inst) +} -//go:embed io.lima-vm.autostart.INSTANCE.plist -var launchdTemplate string +// RegisterToStartAtLogin creates a start-at-login entry for the instance. +func RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error { + return manager().RegisterToStartAtLogin(ctx, inst) +} -// CreateStartAtLoginEntry respect host OS arch and create unit file. -func CreateStartAtLoginEntry(ctx context.Context, hostOS, instName, workDir string) error { - unitPath := GetFilePath(hostOS, instName) - if _, err := os.Stat(unitPath); err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - tmpl, err := renderTemplate(hostOS, instName, workDir, os.Executable) - if err != nil { - return err - } - if err := os.MkdirAll(filepath.Dir(unitPath), os.ModePerm); err != nil { - return err - } - if err := os.WriteFile(unitPath, tmpl, 0o644); err != nil { - return err - } - return enableDisableService(ctx, "enable", hostOS, GetFilePath(hostOS, instName)) +// UnregisterFromStartAtLogin deletes the start-at-login entry for the instance. +func UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error { + return manager().UnregisterFromStartAtLogin(ctx, inst) } -// DeleteStartAtLoginEntry respect host OS arch and delete unit file. -// Return true, nil if unit file has been deleted. -func DeleteStartAtLoginEntry(ctx context.Context, hostOS, instName string) (bool, error) { - unitPath := GetFilePath(hostOS, instName) - if _, err := os.Stat(unitPath); err != nil { - return false, err - } - if err := enableDisableService(ctx, "disable", hostOS, GetFilePath(hostOS, instName)); err != nil { - return false, err - } - if err := os.Remove(unitPath); err != nil { - return false, err - } - return true, nil +// AutoStartedIdentifier returns the identifier if the current process was started by the autostart manager. +func AutoStartedIdentifier() string { + return manager().AutoStartedIdentifier() } -// GetFilePath returns the path to autostart file with respect of host. -func GetFilePath(hostOS, instName string) string { - var fileTmpl string - if hostOS == "darwin" { // launchd plist - fileTmpl = fmt.Sprintf("%s/Library/LaunchAgents/io.lima-vm.autostart.%s.plist", os.Getenv("HOME"), instName) - } - if hostOS == "linux" { // systemd service - // Use instance name as argument to systemd service - // Instance name available in unit file as %i - xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") - if xdgConfigHome == "" { - xdgConfigHome = filepath.Join(os.Getenv("HOME"), ".config") - } - fileTmpl = fmt.Sprintf("%s/systemd/user/lima-vm@%s.service", xdgConfigHome, instName) - } - return fileTmpl +// RequestStart requests to start the instance by identifier. +func RequestStart(ctx context.Context, inst *limatype.Instance) error { + return manager().RequestStart(ctx, inst) } -func enableDisableService(ctx context.Context, action, hostOS, serviceWithPath string) error { - // Get filename without extension - filename := strings.TrimSuffix(path.Base(serviceWithPath), filepath.Ext(path.Base(serviceWithPath))) +// RequestStop requests to stop the instance by identifier. +func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { + return manager().RequestStop(ctx, inst) +} - var args []string - if hostOS == "darwin" { - // man launchctl - args = append(args, []string{ - "launchctl", - action, - fmt.Sprintf("gui/%s/%s", strconv.Itoa(os.Getuid()), filename), - }...) - } else { - args = append(args, []string{ - "systemctl", - "--user", - action, - filename, - }...) - } - cmd := exec.CommandContext(ctx, args[0], args[1:]...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() +type autoStartManager interface { + // Registration + IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error) + RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error + UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error + + // Status + AutoStartedIdentifier() string + + // Operation + // RequestStart requests to start the instance by identifier. + RequestStart(ctx context.Context, inst *limatype.Instance) error + // RequestStop requests to stop the instance by identifier. + RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) } -func renderTemplate(hostOS, instName, workDir string, getExecutable func() (string, error)) ([]byte, error) { - selfExeAbs, err := getExecutable() - if err != nil { - return nil, err - } - tmpToExecute := systemdTemplate - if hostOS == "darwin" { - tmpToExecute = launchdTemplate +var manager = sync.OnceValue(func() autoStartManager { + switch runtime.GOOS { + case "darwin": + return Launchd + case "linux": + if systemd.IsRunningSystemd() { + return Systemd + } + // TODO: support other init systems } - return textutil.ExecuteTemplate( - tmpToExecute, - map[string]string{ - "Binary": selfExeAbs, - "Instance": instName, - "WorkDir": workDir, - }) -} + return ¬SupportedManager{} +}) diff --git a/pkg/autostart/autostart_test.go b/pkg/autostart/autostart_test.go index d4070244272..8080e4e1e43 100644 --- a/pkg/autostart/autostart_test.go +++ b/pkg/autostart/autostart_test.go @@ -5,7 +5,6 @@ package autostart import ( "runtime" - "strings" "testing" "gotest.tools/v3/assert" @@ -16,17 +15,17 @@ func TestRenderTemplate(t *testing.T) { t.Skip("skipping testing on windows host") } tests := []struct { + Manager *TemplateFileBasedManager Name string InstanceName string - HostOS string Expected string WorkDir string GetExecutable func() (string, error) }{ { + Manager: Launchd, Name: "render darwin launchd plist", InstanceName: "default", - HostOS: "darwin", Expected: ` @@ -39,6 +38,7 @@ func TestRenderTemplate(t *testing.T) { start default --foreground + --progress RunAtLoad @@ -58,15 +58,15 @@ func TestRenderTemplate(t *testing.T) { WorkDir: "/some/path", }, { + Manager: Systemd, Name: "render linux systemd service", InstanceName: "default", - HostOS: "linux", Expected: `[Unit] Description=Lima - Linux virtual machines, with a focus on running containers. Documentation=man:lima(1) [Service] -ExecStart=/limactl start %i --foreground +ExecStart=/limactl start %i --foreground --progress WorkingDirectory=%h Type=simple TimeoutSec=10 @@ -82,46 +82,9 @@ WantedBy=default.target`, } for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - tmpl, err := renderTemplate(tt.HostOS, tt.InstanceName, tt.WorkDir, tt.GetExecutable) + tmpl, err := tt.Manager.renderTemplate(tt.InstanceName, tt.WorkDir, tt.GetExecutable) assert.NilError(t, err) assert.Equal(t, string(tmpl), tt.Expected) }) } } - -func TestGetFilePath(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("skipping testing on windows host") - } - tests := []struct { - Name string - HostOS string - InstanceName string - HomeEnv string - Expected string - }{ - { - Name: "darwin with docker instance name", - HostOS: "darwin", - InstanceName: "docker", - Expected: "Library/LaunchAgents/io.lima-vm.autostart.docker.plist", - }, - { - Name: "linux with docker instance name", - HostOS: "linux", - InstanceName: "docker", - Expected: ".config/systemd/user/lima-vm@docker.service", - }, - { - Name: "empty with empty instance name", - HostOS: "", - InstanceName: "", - Expected: "", - }, - } - for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { - assert.Check(t, strings.HasSuffix(GetFilePath(tt.HostOS, tt.InstanceName), tt.Expected)) - }) - } -} diff --git a/pkg/autostart/io.lima-vm.autostart.INSTANCE.plist b/pkg/autostart/launchd/io.lima-vm.autostart.INSTANCE.plist similarity index 95% rename from pkg/autostart/io.lima-vm.autostart.INSTANCE.plist rename to pkg/autostart/launchd/io.lima-vm.autostart.INSTANCE.plist index 7e0ffd9494c..379310884dc 100644 --- a/pkg/autostart/io.lima-vm.autostart.INSTANCE.plist +++ b/pkg/autostart/launchd/io.lima-vm.autostart.INSTANCE.plist @@ -10,6 +10,7 @@ start {{ .Instance }} --foreground + --progress RunAtLoad diff --git a/pkg/autostart/launchd/launchd.go b/pkg/autostart/launchd/launchd.go new file mode 100644 index 00000000000..3ec52980ed2 --- /dev/null +++ b/pkg/autostart/launchd/launchd.go @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package launchd + +import ( + "context" + _ "embed" + "fmt" + "os" + "os/exec" + "sync" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/limatype" +) + +//go:embed io.lima-vm.autostart.INSTANCE.plist +var Template string + +// GetPlistPath returns the path to the launchd plist file for the given instance name. +func GetPlistPath(instName string) string { + return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", os.Getenv("HOME"), ServiceNameFrom(instName)) +} + +// ServiceNameFrom returns the launchd service name for the given instance name. +func ServiceNameFrom(instName string) string { + return fmt.Sprintf("io.lima-vm.autostart.%s", instName) +} + +// EnableDisableService enables or disables the launchd service for the given instance name. +func EnableDisableService(ctx context.Context, enable bool, instName string) error { + action := "enable" + if !enable { + action = "disable" + } + return launchctl(ctx, action, serviceTarget(instName)) +} + +func launchctl(ctx context.Context, args ...string) error { + cmd := exec.CommandContext(ctx, "launchctl", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + logrus.Debugf("running command: %v", cmd.Args) + return cmd.Run() +} + +func launchctlWithoutOutput(ctx context.Context, args ...string) error { + cmd := exec.CommandContext(ctx, "launchctl", args...) + logrus.Debugf("running command without output: %v", cmd.Args) + return cmd.Run() +} + +// AutoStartedServiceName returns the launchd service name if the instance is started by launchd. +func AutoStartedServiceName() string { + // Assume the instance is started by launchd if XPC_SERVICE_NAME is set and not "0". + // To confirm it is actually started by launchd, it needs to use `launch_activate_socket`. + // But that requires actual socket activation setup in the plist file. + // So we just check XPC_SERVICE_NAME here. + if xpcServiceName := os.Getenv("XPC_SERVICE_NAME"); xpcServiceName != "0" { + return xpcServiceName + } + return "" +} + +var domainTarget = sync.OnceValue(func() string { + return fmt.Sprintf("gui/%d", os.Getuid()) +}) + +func serviceTarget(instName string) string { + return fmt.Sprintf("%s/%s", domainTarget(), ServiceNameFrom(instName)) +} + +func RequestStart(ctx context.Context, inst *limatype.Instance) error { + // Call `launchctl bootout` first, because instance may be stopped without unloading the plist file. + // If the plist file is not unloaded, `launchctl bootstrap` will fail. + _ = launchctlWithoutOutput(ctx, "bootout", serviceTarget(inst.Name)) + // If disabled, `launchctl bootstrap` will fail. + _ = EnableDisableService(ctx, true, inst.Name) + if err := launchctl(ctx, "bootstrap", domainTarget(), GetPlistPath(inst.Name)); err != nil { + return fmt.Errorf("failed to start the instance %q via launchctl: %w", inst.Name, err) + } + return nil +} + +func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { + logrus.Debugf("AutoStartedIdentifier=%q, ServiceNameFrom=%q", inst.AutoStartedIdentifier, ServiceNameFrom(inst.Name)) + if inst.AutoStartedIdentifier == ServiceNameFrom(inst.Name) { + logrus.Infof("Stopping the instance %q started by launchd", inst.Name) + if err := launchctl(ctx, "bootout", serviceTarget(inst.Name)); err != nil { + return false, fmt.Errorf("failed to stop the instance %q via launchctl: %w", inst.Name, err) + } + return true, nil + } + return false, nil +} diff --git a/pkg/autostart/launchd/launchd_test.go b/pkg/autostart/launchd/launchd_test.go new file mode 100644 index 00000000000..465f53eda4a --- /dev/null +++ b/pkg/autostart/launchd/launchd_test.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package launchd + +import ( + "runtime" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestGetPlistPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping testing on windows host") + } + tests := []struct { + Name string + InstanceName string + Expected string + }{ + { + Name: "darwin with docker instance name", + InstanceName: "docker", + Expected: "Library/LaunchAgents/io.lima-vm.autostart.docker.plist", + }, + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + assert.Check(t, strings.HasSuffix(GetPlistPath(tt.InstanceName), tt.Expected)) + }) + } +} diff --git a/pkg/autostart/managers.go b/pkg/autostart/managers.go new file mode 100644 index 00000000000..e76eb3f5d80 --- /dev/null +++ b/pkg/autostart/managers.go @@ -0,0 +1,167 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +// Package autostart manage start at login unit files for darwin/linux +package autostart + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + + "github.com/lima-vm/lima/v2/pkg/autostart/launchd" + "github.com/lima-vm/lima/v2/pkg/autostart/systemd" + "github.com/lima-vm/lima/v2/pkg/limatype" + "github.com/lima-vm/lima/v2/pkg/textutil" +) + +type notSupportedManager struct{} + +var ErrNotSupported = fmt.Errorf("autostart is not supported on %s", runtime.GOOS) + +func (*notSupportedManager) IsRegistered(_ context.Context, _ *limatype.Instance) (bool, error) { + return false, ErrNotSupported +} + +func (*notSupportedManager) RegisterToStartAtLogin(_ context.Context, _ *limatype.Instance) error { + return ErrNotSupported +} + +func (*notSupportedManager) UnregisterFromStartAtLogin(_ context.Context, _ *limatype.Instance) error { + return ErrNotSupported +} + +func (*notSupportedManager) AutoStartedIdentifier() string { + return "" +} + +func (*notSupportedManager) RequestStart(_ context.Context, _ *limatype.Instance) error { + return ErrNotSupported +} + +func (*notSupportedManager) RequestStop(_ context.Context, _ *limatype.Instance) (bool, error) { + return false, ErrNotSupported +} + +// Launchd is the autostart manager for macOS. +var Launchd = &TemplateFileBasedManager{ + filePath: launchd.GetPlistPath, + template: launchd.Template, + enabler: launchd.EnableDisableService, + autoStartedIdentifier: launchd.AutoStartedServiceName, + requestStart: launchd.RequestStart, + requestStop: launchd.RequestStop, +} + +// Systemd is the autostart manager for Linux. +var Systemd = &TemplateFileBasedManager{ + filePath: systemd.GetUnitPath, + template: systemd.Template, + enabler: systemd.EnableDisableUnit, + autoStartedIdentifier: systemd.AutoStartedUnitName, + requestStart: systemd.RequestStart, + requestStop: systemd.RequestStop, +} + +// TemplateFileBasedManager is an autostart manager that uses a template file to create the autostart entry. +type TemplateFileBasedManager struct { + enabler func(ctx context.Context, enable bool, instName string) error + filePath func(instName string) string + template string + autoStartedIdentifier func() string + requestStart func(ctx context.Context, inst *limatype.Instance) error + requestStop func(ctx context.Context, inst *limatype.Instance) (bool, error) +} + +func (t *TemplateFileBasedManager) IsRegistered(_ context.Context, inst *limatype.Instance) (bool, error) { + if t.filePath == nil { + return false, errors.New("no filePath function available") + } + autostartFilePath := t.filePath(inst.Name) + if _, err := os.Stat(autostartFilePath); err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + return true, nil +} + +func (t *TemplateFileBasedManager) RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error { + if _, err := t.IsRegistered(ctx, inst); err != nil { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } + content, err := t.renderTemplate(inst.Name, inst.Dir, os.Executable) + if err != nil { + return fmt.Errorf("failed to render the autostart entry for instance %q: %w", inst.Name, err) + } + entryFilePath := t.filePath(inst.Name) + if err := os.MkdirAll(filepath.Dir(entryFilePath), os.ModePerm); err != nil { + return fmt.Errorf("failed to create the directory for the autostart entry for instance %q: %w", inst.Name, err) + } + if err := os.WriteFile(entryFilePath, content, 0o644); err != nil { + return fmt.Errorf("failed to write the autostart entry for instance %q: %w", inst.Name, err) + } + if t.enabler != nil { + return t.enabler(ctx, true, inst.Name) + } + return nil +} + +func (t *TemplateFileBasedManager) UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error { + if registered, err := t.IsRegistered(ctx, inst); err != nil { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if !registered { + return nil + } + if t.enabler != nil { + if err := t.enabler(ctx, false, inst.Name); err != nil { + return fmt.Errorf("failed to disable the autostart entry for instance %q: %w", inst.Name, err) + } + } + if err := os.Remove(t.filePath(inst.Name)); err != nil { + return fmt.Errorf("failed to remove the autostart entry for instance %q: %w", inst.Name, err) + } + return nil +} + +func (t *TemplateFileBasedManager) renderTemplate(instName, workDir string, getExecutable func() (string, error)) ([]byte, error) { + if t.template == "" { + return nil, errors.New("no template available") + } + selfExeAbs, err := getExecutable() + if err != nil { + return nil, err + } + return textutil.ExecuteTemplate( + t.template, + map[string]string{ + "Binary": selfExeAbs, + "Instance": instName, + "WorkDir": workDir, + }) +} + +func (t *TemplateFileBasedManager) AutoStartedIdentifier() string { + if t.autoStartedIdentifier != nil { + return t.autoStartedIdentifier() + } + return "" +} + +func (t *TemplateFileBasedManager) RequestStart(ctx context.Context, inst *limatype.Instance) error { + if t.requestStart == nil { + return errors.New("no RequestStart function available") + } + return t.requestStart(ctx, inst) +} + +func (t *TemplateFileBasedManager) RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { + if t.requestStop == nil { + return false, errors.New("no RequestStop function available") + } + return t.requestStop(ctx, inst) +} diff --git a/pkg/autostart/lima-vm@INSTANCE.service b/pkg/autostart/systemd/lima-vm@INSTANCE.service similarity index 80% rename from pkg/autostart/lima-vm@INSTANCE.service rename to pkg/autostart/systemd/lima-vm@INSTANCE.service index b22d03ba2d9..07f59fc1e24 100644 --- a/pkg/autostart/lima-vm@INSTANCE.service +++ b/pkg/autostart/systemd/lima-vm@INSTANCE.service @@ -3,7 +3,7 @@ Description=Lima - Linux virtual machines, with a focus on running containers. Documentation=man:lima(1) [Service] -ExecStart={{.Binary}} start %i --foreground +ExecStart={{.Binary}} start %i --foreground --progress WorkingDirectory=%h Type=simple TimeoutSec=10 diff --git a/pkg/autostart/systemd/systemd.go b/pkg/autostart/systemd/systemd.go new file mode 100644 index 00000000000..96962d042f4 --- /dev/null +++ b/pkg/autostart/systemd/systemd.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package systemd + +import ( + "context" + _ "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/sirupsen/logrus" + + "github.com/lima-vm/lima/v2/pkg/limatype" +) + +//go:embed lima-vm@INSTANCE.service +var Template string + +// GetUnitPath returns the path to the systemd unit file for the given instance name. +func GetUnitPath(instName string) string { + // Use instance name as argument to systemd service + // Instance name available in unit file as %i + xdgConfigHome := os.Getenv("XDG_CONFIG_HOME") + if xdgConfigHome == "" { + xdgConfigHome = filepath.Join(os.Getenv("HOME"), ".config") + } + return fmt.Sprintf("%s/systemd/user/%s", xdgConfigHome, UnitNameFrom(instName)) +} + +// UnitNameFrom returns the systemd service name for the given instance name. +func UnitNameFrom(instName string) string { + return fmt.Sprintf("lima-vm@%s.service", instName) +} + +// EnableDisableUnit enables or disables the systemd service for the given instance name. +func EnableDisableUnit(ctx context.Context, enable bool, instName string) error { + action := "enable" + if !enable { + action = "disable" + } + return systemctl(ctx, "--user", action, UnitNameFrom(instName)) +} + +func systemctl(ctx context.Context, args ...string) error { + cmd := exec.CommandContext(ctx, "systemctl", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + logrus.Debugf("running command: %v", cmd.Args) + return cmd.Run() +} + +// AutoStartedUnitName returns the systemd service name if the instance is started by systemd. +func AutoStartedUnitName() string { + return CurrentUnitName() +} + +func RequestStart(ctx context.Context, inst *limatype.Instance) error { + if err := systemctl(ctx, "--user", "start", UnitNameFrom(inst.Name)); err != nil { + return fmt.Errorf("failed to start the instance %q via systemctl: %w", inst.Name, err) + } + return nil +} + +func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { + if inst.AutoStartedIdentifier == UnitNameFrom(inst.Name) { + logrus.Infof("Stopping the instance %q started by systemd", inst.Name) + if err := systemctl(ctx, "--user", "stop", inst.AutoStartedIdentifier); err != nil { + return false, fmt.Errorf("failed to stop the instance %q via systemctl: %w", inst.Name, err) + } + return true, nil + } + return false, nil +} diff --git a/pkg/autostart/systemd/systemd_linux.go b/pkg/autostart/systemd/systemd_linux.go new file mode 100644 index 00000000000..35f4d25a8e5 --- /dev/null +++ b/pkg/autostart/systemd/systemd_linux.go @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package systemd + +import ( + "github.com/coreos/go-systemd/v22/util" + "github.com/sirupsen/logrus" +) + +func CurrentUnitName() string { + unit, err := util.CurrentUnitName() + if err != nil { + logrus.WithError(err).Debug("cannot determine current systemd unit name") + } + return unit +} + +func IsRunningSystemd() bool { + return util.IsRunningSystemd() +} diff --git a/pkg/autostart/systemd/systemd_others.go b/pkg/autostart/systemd/systemd_others.go new file mode 100644 index 00000000000..4c577c3e08d --- /dev/null +++ b/pkg/autostart/systemd/systemd_others.go @@ -0,0 +1,14 @@ +//go:build !linux + +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package systemd + +func CurrentUnitName() string { + return "" +} + +func IsRunningSystemd() bool { + return false +} diff --git a/pkg/autostart/systemd/systemd_test.go b/pkg/autostart/systemd/systemd_test.go new file mode 100644 index 00000000000..fe9c6d55f31 --- /dev/null +++ b/pkg/autostart/systemd/systemd_test.go @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package systemd + +import ( + "runtime" + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +func TestGetUnitPath(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping testing on windows host") + } + tests := []struct { + Name string + InstanceName string + Expected string + }{ + { + Name: "linux with docker instance name", + InstanceName: "docker", + Expected: ".config/systemd/user/lima-vm@docker.service", + }, + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + assert.Check(t, strings.HasSuffix(GetUnitPath(tt.InstanceName), tt.Expected)) + }) + } +} diff --git a/pkg/hostagent/api/api.go b/pkg/hostagent/api/api.go index f86de58e5d7..d893b6acddd 100644 --- a/pkg/hostagent/api/api.go +++ b/pkg/hostagent/api/api.go @@ -4,5 +4,8 @@ package api type Info struct { + // indicate instance is started by launchd or systemd if not empty + AutoStartedIdentifier string `json:"autoStartedIdentifier,omitempty"` + // SSHLocalPort is the local port on the host for SSH access to the VM. SSHLocalPort int `json:"sshLocalPort,omitempty"` } diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go index 50cdf9edacf..a32f6a778a7 100644 --- a/pkg/hostagent/hostagent.go +++ b/pkg/hostagent/hostagent.go @@ -27,6 +27,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/cidata" "github.com/lima-vm/lima/v2/pkg/driver" "github.com/lima-vm/lima/v2/pkg/driverutil" @@ -478,7 +479,8 @@ func (a *HostAgent) startRoutinesAndWait(ctx context.Context, errCh <-chan error func (a *HostAgent) Info(_ context.Context) (*hostagentapi.Info, error) { info := &hostagentapi.Info{ - SSHLocalPort: a.sshLocalPort, + AutoStartedIdentifier: autostart.AutoStartedIdentifier(), + SSHLocalPort: a.sshLocalPort, } return info, nil } diff --git a/pkg/instance/restart.go b/pkg/instance/restart.go index 8797f03a436..13b01faed77 100644 --- a/pkg/instance/restart.go +++ b/pkg/instance/restart.go @@ -5,9 +5,12 @@ package instance import ( "context" + "errors" + "fmt" "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/limatype" networks "github.com/lima-vm/lima/v2/pkg/networks/reconcile" ) @@ -21,8 +24,13 @@ func Restart(ctx context.Context, inst *limatype.Instance, showProgress bool) er return err } - if err := networks.Reconcile(ctx, inst.Name); err != nil { - return err + // Network reconciliation will be performed by the process launched by the autostart manager + if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if !registered { + if err := networks.Reconcile(ctx, inst.Name); err != nil { + return err + } } if err := Start(ctx, inst, launchHostAgentForeground, showProgress); err != nil { @@ -36,8 +44,12 @@ func RestartForcibly(ctx context.Context, inst *limatype.Instance, showProgress logrus.Info("Restarting the instance forcibly") StopForcibly(inst) - if err := networks.Reconcile(ctx, inst.Name); err != nil { - return err + if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err) + } else if !registered { + if err := networks.Reconcile(ctx, inst.Name); err != nil { + return err + } } if err := Start(ctx, inst, launchHostAgentForeground, showProgress); err != nil { diff --git a/pkg/instance/start.go b/pkg/instance/start.go index f29db3ed51d..d837c633f3c 100644 --- a/pkg/instance/start.go +++ b/pkg/instance/start.go @@ -19,6 +19,7 @@ import ( "github.com/lima-vm/go-qcow2reader" "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/autostart" "github.com/lima-vm/lima/v2/pkg/cacheutil" "github.com/lima-vm/lima/v2/pkg/driver" "github.com/lima-vm/lima/v2/pkg/driverutil" @@ -169,63 +170,70 @@ func StartWithPaths(ctx context.Context, inst *limatype.Instance, launchHostAgen } haStdoutPath := filepath.Join(inst.Dir, filenames.HostAgentStdoutLog) haStderrPath := filepath.Join(inst.Dir, filenames.HostAgentStderrLog) - if err := os.RemoveAll(haStdoutPath); err != nil { - return err - } - if err := os.RemoveAll(haStderrPath); err != nil { - return err - } - haStdoutW, err := os.Create(haStdoutPath) - if err != nil { - return err - } - // no defer haStdoutW.Close() - haStderrW, err := os.Create(haStderrPath) - if err != nil { - return err - } - // no defer haStderrW.Close() - var args []string - if logrus.GetLevel() >= logrus.DebugLevel { - args = append(args, "--debug") - } - args = append(args, - "hostagent", - "--pidfile", haPIDPath, - "--socket", haSockPath) - if prepared.Driver.Info().Features.CanRunGUI { - args = append(args, "--run-gui") - } - if prepared.GuestAgent != "" { - args = append(args, "--guestagent", prepared.GuestAgent) - } - if prepared.NerdctlArchiveCache != "" { - args = append(args, "--nerdctl-archive", prepared.NerdctlArchiveCache) - } - if showProgress { - args = append(args, "--progress") - } - args = append(args, inst.Name) - haCmd := exec.CommandContext(ctx, limactl, args...) + begin := time.Now() // used for logrus propagation + var haCmd *exec.Cmd + if isRegisteredToAutoStart, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to check autostart registration: %w", err) + } else if !isRegisteredToAutoStart || launchHostAgentForeground { + if err := os.RemoveAll(haStdoutPath); err != nil { + return err + } + if err := os.RemoveAll(haStderrPath); err != nil { + return err + } + haStdoutW, err := os.Create(haStdoutPath) + if err != nil { + return err + } + // no defer haStdoutW.Close() + haStderrW, err := os.Create(haStderrPath) + if err != nil { + return err + } + // no defer haStderrW.Close() - if launchHostAgentForeground { - haCmd.SysProcAttr = executil.ForegroundSysProcAttr - } else { - haCmd.SysProcAttr = executil.BackgroundSysProcAttr - } + var args []string + if logrus.GetLevel() >= logrus.DebugLevel { + args = append(args, "--debug") + } + args = append(args, + "hostagent", + "--pidfile", haPIDPath, + "--socket", haSockPath) + if prepared.Driver.Info().Features.CanRunGUI { + args = append(args, "--run-gui") + } + if prepared.GuestAgent != "" { + args = append(args, "--guestagent", prepared.GuestAgent) + } + if prepared.NerdctlArchiveCache != "" { + args = append(args, "--nerdctl-archive", prepared.NerdctlArchiveCache) + } + if showProgress { + args = append(args, "--progress") + } + args = append(args, inst.Name) + haCmd = exec.CommandContext(ctx, limactl, args...) - haCmd.Stdout = haStdoutW - haCmd.Stderr = haStderrW + haCmd.SysProcAttr = executil.BackgroundSysProcAttr - begin := time.Now() // used for logrus propagation + haCmd.Stdout = haStdoutW + haCmd.Stderr = haStderrW - if launchHostAgentForeground { - if err := execHostAgentForeground(limactl, haCmd); err != nil { + if launchHostAgentForeground { + if isRegisteredToAutoStart { + logrus.Warn("The instance is registered to start at login, but the --foreground option was given, so starting the instance directly") + } + haCmd.SysProcAttr = executil.ForegroundSysProcAttr + if err := execHostAgentForeground(limactl, haCmd); err != nil { + return err + } + } else if err := haCmd.Start(); err != nil { return err } - } else if err := haCmd.Start(); err != nil { - return err + } else if err = autostart.RequestStart(ctx, inst); err != nil { + return fmt.Errorf("failed to request start via autostart manager: %w", err) } if err := waitHostAgentStart(ctx, haPIDPath, haStderrPath); err != nil { @@ -238,10 +246,14 @@ func StartWithPaths(ctx context.Context, inst *limatype.Instance, launchHostAgen close(watchErrCh) }() waitErrCh := make(chan error) - go func() { - waitErrCh <- haCmd.Wait() - close(waitErrCh) - }() + if haCmd != nil { + go func() { + waitErrCh <- haCmd.Wait() + close(waitErrCh) + }() + } else { + defer close(waitErrCh) + } select { case watchErr := <-watchErrCh: diff --git a/pkg/instance/stop.go b/pkg/instance/stop.go index 0b6770808c7..af4605b85ec 100644 --- a/pkg/instance/stop.go +++ b/pkg/instance/stop.go @@ -14,6 +14,7 @@ import ( "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/autostart" hostagentevents "github.com/lima-vm/lima/v2/pkg/hostagent/events" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" @@ -31,9 +32,13 @@ func StopGracefully(ctx context.Context, inst *limatype.Instance, isRestart bool } begin := time.Now() // used for logrus propagation - logrus.Infof("Sending SIGINT to hostagent process %d", inst.HostAgentPID) - if err := osutil.SysKill(inst.HostAgentPID, osutil.SigInt); err != nil { - logrus.Error(err) + if requested, err := autostart.RequestStop(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) { + return fmt.Errorf("failed to request stop via autostart manager: %w", err) + } else if !requested { + logrus.Infof("Sending SIGINT to hostagent process %d", inst.HostAgentPID) + if err := osutil.SysKill(inst.HostAgentPID, osutil.SigInt); err != nil { + logrus.Error(err) + } } logrus.Info("Waiting for the host agent and the driver processes to shut down") diff --git a/pkg/limatype/lima_instance.go b/pkg/limatype/lima_instance.go index 308dd21ea51..fada2f34e98 100644 --- a/pkg/limatype/lima_instance.go +++ b/pkg/limatype/lima_instance.go @@ -26,27 +26,28 @@ const ( type Instance struct { Name string `json:"name"` // Hostname, not HostName (corresponds to SSH's naming convention) - Hostname string `json:"hostname"` - Status Status `json:"status"` - Dir string `json:"dir"` - VMType VMType `json:"vmType"` - Arch Arch `json:"arch"` - CPUs int `json:"cpus,omitempty"` - Memory int64 `json:"memory,omitempty"` // bytes - Disk int64 `json:"disk,omitempty"` // bytes - Message string `json:"message,omitempty"` - AdditionalDisks []Disk `json:"additionalDisks,omitempty"` - Networks []Network `json:"network,omitempty"` - SSHLocalPort int `json:"sshLocalPort,omitempty"` - SSHConfigFile string `json:"sshConfigFile,omitempty"` - HostAgentPID int `json:"hostAgentPID,omitempty"` - DriverPID int `json:"driverPID,omitempty"` - Errors []error `json:"errors,omitempty"` - Config *LimaYAML `json:"config,omitempty"` - SSHAddress string `json:"sshAddress,omitempty"` - Protected bool `json:"protected"` - LimaVersion string `json:"limaVersion"` - Param map[string]string `json:"param,omitempty"` + Hostname string `json:"hostname"` + Status Status `json:"status"` + Dir string `json:"dir"` + VMType VMType `json:"vmType"` + Arch Arch `json:"arch"` + CPUs int `json:"cpus,omitempty"` + Memory int64 `json:"memory,omitempty"` // bytes + Disk int64 `json:"disk,omitempty"` // bytes + Message string `json:"message,omitempty"` + AdditionalDisks []Disk `json:"additionalDisks,omitempty"` + Networks []Network `json:"network,omitempty"` + SSHLocalPort int `json:"sshLocalPort,omitempty"` + SSHConfigFile string `json:"sshConfigFile,omitempty"` + HostAgentPID int `json:"hostAgentPID,omitempty"` + DriverPID int `json:"driverPID,omitempty"` + Errors []error `json:"errors,omitempty"` + Config *LimaYAML `json:"config,omitempty"` + SSHAddress string `json:"sshAddress,omitempty"` + Protected bool `json:"protected"` + LimaVersion string `json:"limaVersion"` + Param map[string]string `json:"param,omitempty"` + AutoStartedIdentifier string `json:"autoStartedIdentifier,omitempty"` } // Protect protects the instance to prohibit accidental removal. diff --git a/pkg/store/instance.go b/pkg/store/instance.go index dc2f3319de4..54e7a9c1f3e 100644 --- a/pkg/store/instance.go +++ b/pkg/store/instance.go @@ -83,6 +83,7 @@ func Inspect(ctx context.Context, instName string) (*limatype.Instance, error) { inst.Errors = append(inst.Errors, fmt.Errorf("failed to get Info from %q: %w", haSock, err)) } else { inst.SSHLocalPort = info.SSHLocalPort + inst.AutoStartedIdentifier = info.AutoStartedIdentifier } } }