Skip to content

Commit 1f4369d

Browse files
committed
autostart: Refactor to add autoStartManager
- Hide creating/deleting an autostart entry file into `Register`/`Unregister` as the implementation details. - Separate the `launchd` and `systemd`-specific code into packages. Signed-off-by: Norio Nomura <[email protected]>
1 parent c71bc48 commit 1f4369d

File tree

11 files changed

+344
-163
lines changed

11 files changed

+344
-163
lines changed

cmd/limactl/delete.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"errors"
88
"fmt"
99
"os"
10-
"runtime"
1110

1211
"github.com/sirupsen/logrus"
1312
"github.com/spf13/cobra"
@@ -50,13 +49,16 @@ func deleteAction(cmd *cobra.Command, args []string) error {
5049
if err := instance.Delete(cmd.Context(), inst, force); err != nil {
5150
return fmt.Errorf("failed to delete instance %q: %w", instName, err)
5251
}
53-
if runtime.GOOS == "darwin" || runtime.GOOS == "linux" {
54-
deleted, err := autostart.DeleteStartAtLoginEntry(ctx, runtime.GOOS, instName)
55-
if err != nil && !errors.Is(err, os.ErrNotExist) {
56-
logrus.WithError(err).Warnf("The autostart file for instance %q does not exist", instName)
57-
} else if deleted {
58-
logrus.Infof("The autostart file %q has been deleted", autostart.GetFilePath(runtime.GOOS, instName))
52+
if registered, err := autostart.IsRegistered(ctx, inst); err != nil {
53+
logrus.WithError(err).Warnf("Failed to check if the autostart entry for instance %q is registered", instName)
54+
} else if registered {
55+
if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {
56+
logrus.WithError(err).Warnf("Failed to unregister the autostart entry for instance %q", instName)
57+
} else {
58+
logrus.Infof("The autostart entry for instance %q has been unregistered", instName)
5959
}
60+
} else {
61+
logrus.Infof("The autostart entry for instance %q is not registered", instName)
6062
}
6163
logrus.Infof("Deleted %q (%q)", instName, inst.Dir)
6264
}

cmd/limactl/start-at-login_unix.go

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ package main
77

88
import (
99
"errors"
10+
"fmt"
1011
"os"
11-
"runtime"
1212

1313
"github.com/sirupsen/logrus"
1414
"github.com/spf13/cobra"
@@ -38,18 +38,24 @@ func startAtLoginAction(cmd *cobra.Command, args []string) error {
3838
if err != nil {
3939
return err
4040
}
41-
if startAtLogin {
42-
if err := autostart.CreateStartAtLoginEntry(ctx, runtime.GOOS, inst.Name, inst.Dir); err != nil {
43-
logrus.WithError(err).Warnf("Can't create an autostart file for instance %q", inst.Name)
44-
} else {
45-
logrus.Infof("The autostart file %q has been created or updated", autostart.GetFilePath(runtime.GOOS, inst.Name))
41+
if registered, err := autostart.IsRegistered(ctx, inst); err != nil {
42+
return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err)
43+
} else if startAtLogin {
44+
verb := "create"
45+
if registered {
46+
verb = "update"
47+
}
48+
if err := autostart.RegisterToStartAtLogin(ctx, inst); err != nil {
49+
return fmt.Errorf("failed to %s the autostart entry for instance %q: %w", verb, inst.Name, err)
4650
}
51+
logrus.Infof("The autostart entry for instance %q has been %sd", inst.Name, verb)
4752
} else {
48-
deleted, err := autostart.DeleteStartAtLoginEntry(ctx, runtime.GOOS, instName)
49-
if err != nil {
50-
logrus.WithError(err).Warnf("The autostart file %q could not be deleted", instName)
51-
} else if deleted {
52-
logrus.Infof("The autostart file %q has been deleted", autostart.GetFilePath(runtime.GOOS, instName))
53+
if !registered {
54+
logrus.Infof("The autostart entry for instance %q is not registered", inst.Name)
55+
} else if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {
56+
return fmt.Errorf("failed to unregister the autostart entry for instance %q: %w", inst.Name, err)
57+
} else {
58+
logrus.Infof("The autostart entry for instance %q has been unregistered", inst.Name)
5359
}
5460
}
5561

pkg/autostart/autostart.go

Lines changed: 26 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -6,118 +6,41 @@ package autostart
66

77
import (
88
"context"
9-
_ "embed"
10-
"errors"
11-
"fmt"
12-
"os"
13-
"os/exec"
14-
"path"
15-
"path/filepath"
16-
"strconv"
17-
"strings"
9+
"runtime"
10+
"sync"
1811

19-
"github.com/lima-vm/lima/v2/pkg/textutil"
12+
"github.com/lima-vm/lima/v2/pkg/limatype"
2013
)
2114

22-
23-
var systemdTemplate string
24-
25-
//go:embed io.lima-vm.autostart.INSTANCE.plist
26-
var launchdTemplate string
27-
28-
// CreateStartAtLoginEntry respect host OS arch and create unit file.
29-
func CreateStartAtLoginEntry(ctx context.Context, hostOS, instName, workDir string) error {
30-
unitPath := GetFilePath(hostOS, instName)
31-
if _, err := os.Stat(unitPath); err != nil && !errors.Is(err, os.ErrNotExist) {
32-
return err
33-
}
34-
tmpl, err := renderTemplate(hostOS, instName, workDir, os.Executable)
35-
if err != nil {
36-
return err
37-
}
38-
if err := os.MkdirAll(filepath.Dir(unitPath), os.ModePerm); err != nil {
39-
return err
40-
}
41-
if err := os.WriteFile(unitPath, tmpl, 0o644); err != nil {
42-
return err
43-
}
44-
return enableDisableService(ctx, "enable", hostOS, GetFilePath(hostOS, instName))
15+
// IsRegistered checks if the instance is registered to start at login.
16+
func IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error) {
17+
return manager().IsRegistered(ctx, inst)
4518
}
4619

47-
// DeleteStartAtLoginEntry respect host OS arch and delete unit file.
48-
// Return true, nil if unit file has been deleted.
49-
func DeleteStartAtLoginEntry(ctx context.Context, hostOS, instName string) (bool, error) {
50-
unitPath := GetFilePath(hostOS, instName)
51-
if _, err := os.Stat(unitPath); err != nil {
52-
return false, err
53-
}
54-
if err := enableDisableService(ctx, "disable", hostOS, GetFilePath(hostOS, instName)); err != nil {
55-
return false, err
56-
}
57-
if err := os.Remove(unitPath); err != nil {
58-
return false, err
59-
}
60-
return true, nil
20+
// RegisterToStartAtLogin creates a start-at-login entry for the instance.
21+
func RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error {
22+
return manager().RegisterToStartAtLogin(ctx, inst)
6123
}
6224

63-
// GetFilePath returns the path to autostart file with respect of host.
64-
func GetFilePath(hostOS, instName string) string {
65-
var fileTmpl string
66-
if hostOS == "darwin" { // launchd plist
67-
fileTmpl = fmt.Sprintf("%s/Library/LaunchAgents/io.lima-vm.autostart.%s.plist", os.Getenv("HOME"), instName)
68-
}
69-
if hostOS == "linux" { // systemd service
70-
// Use instance name as argument to systemd service
71-
// Instance name available in unit file as %i
72-
xdgConfigHome := os.Getenv("XDG_CONFIG_HOME")
73-
if xdgConfigHome == "" {
74-
xdgConfigHome = filepath.Join(os.Getenv("HOME"), ".config")
75-
}
76-
fileTmpl = fmt.Sprintf("%s/systemd/user/lima-vm@%s.service", xdgConfigHome, instName)
77-
}
78-
return fileTmpl
25+
// UnregisterFromStartAtLogin deletes the start-at-login entry for the instance.
26+
func UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error {
27+
return manager().UnregisterFromStartAtLogin(ctx, inst)
7928
}
8029

81-
func enableDisableService(ctx context.Context, action, hostOS, serviceWithPath string) error {
82-
// Get filename without extension
83-
filename := strings.TrimSuffix(path.Base(serviceWithPath), filepath.Ext(path.Base(serviceWithPath)))
84-
85-
var args []string
86-
if hostOS == "darwin" {
87-
// man launchctl
88-
args = append(args, []string{
89-
"launchctl",
90-
action,
91-
fmt.Sprintf("gui/%s/%s", strconv.Itoa(os.Getuid()), filename),
92-
}...)
93-
} else {
94-
args = append(args, []string{
95-
"systemctl",
96-
"--user",
97-
action,
98-
filename,
99-
}...)
100-
}
101-
cmd := exec.CommandContext(ctx, args[0], args[1:]...)
102-
cmd.Stdout = os.Stdout
103-
cmd.Stderr = os.Stderr
104-
return cmd.Run()
30+
type autoStartManager interface {
31+
// Registration
32+
IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error)
33+
RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error
34+
UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error
10535
}
10636

107-
func renderTemplate(hostOS, instName, workDir string, getExecutable func() (string, error)) ([]byte, error) {
108-
selfExeAbs, err := getExecutable()
109-
if err != nil {
110-
return nil, err
111-
}
112-
tmpToExecute := systemdTemplate
113-
if hostOS == "darwin" {
114-
tmpToExecute = launchdTemplate
37+
var manager = sync.OnceValue(func() autoStartManager {
38+
switch runtime.GOOS {
39+
case "darwin":
40+
return Launchd
41+
case "linux":
42+
return Systemd
43+
default:
44+
return &notSupportedManager{}
11545
}
116-
return textutil.ExecuteTemplate(
117-
tmpToExecute,
118-
map[string]string{
119-
"Binary": selfExeAbs,
120-
"Instance": instName,
121-
"WorkDir": workDir,
122-
})
123-
}
46+
})

pkg/autostart/autostart_test.go

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ package autostart
55

66
import (
77
"runtime"
8-
"strings"
98
"testing"
109

1110
"gotest.tools/v3/assert"
@@ -16,17 +15,17 @@ func TestRenderTemplate(t *testing.T) {
1615
t.Skip("skipping testing on windows host")
1716
}
1817
tests := []struct {
18+
Manager *TemplateFileBasedManager
1919
Name string
2020
InstanceName string
21-
HostOS string
2221
Expected string
2322
WorkDir string
2423
GetExecutable func() (string, error)
2524
}{
2625
{
26+
Manager: Launchd,
2727
Name: "render darwin launchd plist",
2828
InstanceName: "default",
29-
HostOS: "darwin",
3029
Expected: `<?xml version="1.0" encoding="UTF-8"?>
3130
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3231
<plist version="1.0">
@@ -58,9 +57,9 @@ func TestRenderTemplate(t *testing.T) {
5857
WorkDir: "/some/path",
5958
},
6059
{
60+
Manager: Systemd,
6161
Name: "render linux systemd service",
6262
InstanceName: "default",
63-
HostOS: "linux",
6463
Expected: `[Unit]
6564
Description=Lima - Linux virtual machines, with a focus on running containers.
6665
Documentation=man:lima(1)
@@ -82,46 +81,9 @@ WantedBy=default.target`,
8281
}
8382
for _, tt := range tests {
8483
t.Run(tt.Name, func(t *testing.T) {
85-
tmpl, err := renderTemplate(tt.HostOS, tt.InstanceName, tt.WorkDir, tt.GetExecutable)
84+
tmpl, err := tt.Manager.renderTemplate(tt.InstanceName, tt.WorkDir, tt.GetExecutable)
8685
assert.NilError(t, err)
8786
assert.Equal(t, string(tmpl), tt.Expected)
8887
})
8988
}
9089
}
91-
92-
func TestGetFilePath(t *testing.T) {
93-
if runtime.GOOS == "windows" {
94-
t.Skip("skipping testing on windows host")
95-
}
96-
tests := []struct {
97-
Name string
98-
HostOS string
99-
InstanceName string
100-
HomeEnv string
101-
Expected string
102-
}{
103-
{
104-
Name: "darwin with docker instance name",
105-
HostOS: "darwin",
106-
InstanceName: "docker",
107-
Expected: "Library/LaunchAgents/io.lima-vm.autostart.docker.plist",
108-
},
109-
{
110-
Name: "linux with docker instance name",
111-
HostOS: "linux",
112-
InstanceName: "docker",
113-
Expected: ".config/systemd/user/[email protected]",
114-
},
115-
{
116-
Name: "empty with empty instance name",
117-
HostOS: "",
118-
InstanceName: "",
119-
Expected: "",
120-
},
121-
}
122-
for _, tt := range tests {
123-
t.Run(tt.Name, func(t *testing.T) {
124-
assert.Check(t, strings.HasSuffix(GetFilePath(tt.HostOS, tt.InstanceName), tt.Expected))
125-
})
126-
}
127-
}
File renamed without changes.

pkg/autostart/launchd/launchd.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package launchd
5+
6+
import (
7+
"context"
8+
_ "embed"
9+
"fmt"
10+
"os"
11+
"os/exec"
12+
13+
"github.com/sirupsen/logrus"
14+
)
15+
16+
//go:embed io.lima-vm.autostart.INSTANCE.plist
17+
var Template string
18+
19+
// GetPlistPath returns the path to the launchd plist file for the given instance name.
20+
func GetPlistPath(instName string) string {
21+
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", os.Getenv("HOME"), ServiceNameFrom(instName))
22+
}
23+
24+
// ServiceNameFrom returns the launchd service name for the given instance name.
25+
func ServiceNameFrom(instName string) string {
26+
return fmt.Sprintf("io.lima-vm.autostart.%s", instName)
27+
}
28+
29+
// EnableDisableService enables or disables the launchd service for the given instance name.
30+
func EnableDisableService(ctx context.Context, enable bool, instName string) error {
31+
action := "enable"
32+
if !enable {
33+
action = "disable"
34+
}
35+
return launchctl(ctx, action, fmt.Sprintf("gui/%d/%s", os.Getuid(), ServiceNameFrom(instName)))
36+
}
37+
38+
func launchctl(ctx context.Context, args ...string) error {
39+
cmd := exec.CommandContext(ctx, "launchctl", args...)
40+
cmd.Stdout = os.Stdout
41+
cmd.Stderr = os.Stderr
42+
logrus.Debugf("running command: %v", cmd.Args)
43+
return cmd.Run()
44+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-FileCopyrightText: Copyright The Lima Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package launchd
5+
6+
import (
7+
"runtime"
8+
"strings"
9+
"testing"
10+
11+
"gotest.tools/v3/assert"
12+
)
13+
14+
func TestGetPlistPath(t *testing.T) {
15+
if runtime.GOOS == "windows" {
16+
t.Skip("skipping testing on windows host")
17+
}
18+
tests := []struct {
19+
Name string
20+
InstanceName string
21+
Expected string
22+
}{
23+
{
24+
Name: "darwin with docker instance name",
25+
InstanceName: "docker",
26+
Expected: "Library/LaunchAgents/io.lima-vm.autostart.docker.plist",
27+
},
28+
}
29+
for _, tt := range tests {
30+
t.Run(tt.Name, func(t *testing.T) {
31+
assert.Check(t, strings.HasSuffix(GetPlistPath(tt.InstanceName), tt.Expected))
32+
})
33+
}
34+
}

0 commit comments

Comments
 (0)