Skip to content

Commit ac9db28

Browse files
committed
autostart: Ensure instance is started/stopped by launchd or systemctl
If the instance isn’t launched by `launchd`, it won’t be stopped on log out. This changes to use `launchctl` or `systemctl` to start/stop the instance if it’s registered to autostart. ## Affected sub commands: - `limactl start` - `limactl stop` - `limactl restart` - `limactl edit` - `limactl shell` ## API changes ### `pkg/autostart` - Introduced `AutoStartedIdentifier()`: - If not empty, it indicates whether the instance was started by `launchd` or `systemd`. - Added `RequestStart()`: - Delegates the operation to `launchd` or `systemd`. - Added `RequestStop()`: - Delegates the operation to `launchd` or `systemd`. ### `pkg/autostart/launchd` - Added `AutoStartedServiceName()`: - Uses the XPC_SERVICE_NAME environment variable as the service name. - Added `RequestStart()`: - Uses `launchctl enable service-target` to avoid failing `bootstrap`. - Uses `launchctl bootstrap domain-target plist-path`. - Added `RequestStop()`: - Uses `launchctl bootout service-target` if the instance is launched by `launchd`. - Added `--progress` to the `limactl` option in `io.lima-vm.autostart.INSTANCE.plist`: - Required to support `limactl start --progress`. ### `pkg/autostart/systemd` - Added `AutoStartedServiceName()`: - Uses `CurrentUnitName()` by `github.com/coreos/go-systemd/v22/util` as the service identifier. - Added `RequestStart()`: - Uses `systemctl --user start unit-name`. - Added `RequestStop()`: - Uses `systemctl --user stop unit-name` if the instance is launched by `systemd`. - Added `--progress` to the `limactl` option in `[email protected]`: - Required to support `limactl start --progress`. ### `pkg/hostagent` - Add `AutoStartedIdentifier` to `Info`. - If not empty, it indicates whether the instance was started by `launchd` or `systemd`. ### `pkg/instance` - `StartWithPaths()`: - Use `autostart.IsRegistered()` to check if the instance is registered to autostart. - If `launchHostAgentForeground` is true, ignore autostart registration. - If the instance is registered to autostart, use `autostart.RequestStart()` instead of launching HostAgent. - `StopGracefully()`: - Use `autostart.RequestStop()`. - `Restart()`, `RestartForcibly()`: - Use `autostart.IsRegistered()` to skip `networks.Reconcile()` if the instance is registered to autostart. Signed-off-by: Norio Nomura <[email protected]>
1 parent acee09f commit ac9db28

22 files changed

+364
-131
lines changed

cmd/limactl/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func deleteAction(cmd *cobra.Command, args []string) error {
4949
if err := instance.Delete(cmd.Context(), inst, force); err != nil {
5050
return fmt.Errorf("failed to delete instance %q: %w", instName, err)
5151
}
52-
if registered, err := autostart.IsRegistered(ctx, inst); err != nil {
52+
if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {
5353
logrus.WithError(err).Warnf("Failed to check if the autostart entry for instance %q is registered", instName)
5454
} else if registered {
5555
if err := autostart.UnregisterFromStartAtLogin(ctx, inst); err != nil {

cmd/limactl/edit.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/spf13/cobra"
1515

1616
"github.com/lima-vm/lima/v2/cmd/limactl/editflags"
17+
"github.com/lima-vm/lima/v2/pkg/autostart"
1718
"github.com/lima-vm/lima/v2/pkg/driverutil"
1819
"github.com/lima-vm/lima/v2/pkg/editutil"
1920
"github.com/lima-vm/lima/v2/pkg/instance"
@@ -155,9 +156,14 @@ func editAction(cmd *cobra.Command, args []string) error {
155156
if !startNow {
156157
return nil
157158
}
158-
err = networks.Reconcile(ctx, inst.Name)
159-
if err != nil {
160-
return err
159+
// Network reconciliation will be performed by the process launched by the autostart manager
160+
if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {
161+
return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err)
162+
} else if !registered {
163+
err = networks.Reconcile(ctx, inst.Name)
164+
if err != nil {
165+
return err
166+
}
161167
}
162168

163169
// store.Inspect() syncs values between inst.YAML and the store.

cmd/limactl/shell.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/sirupsen/logrus"
2121
"github.com/spf13/cobra"
2222

23+
"github.com/lima-vm/lima/v2/pkg/autostart"
2324
"github.com/lima-vm/lima/v2/pkg/envutil"
2425
"github.com/lima-vm/lima/v2/pkg/instance"
2526
"github.com/lima-vm/lima/v2/pkg/ioutilx"
@@ -101,9 +102,14 @@ func shellAction(cmd *cobra.Command, args []string) error {
101102
return nil
102103
}
103104

104-
err = networks.Reconcile(ctx, inst.Name)
105-
if err != nil {
106-
return err
105+
// Network reconciliation will be performed by the process launched by the autostart manager
106+
if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {
107+
return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err)
108+
} else if !registered {
109+
err = networks.Reconcile(ctx, inst.Name)
110+
if err != nil {
111+
return err
112+
}
107113
}
108114

109115
err = instance.Start(ctx, inst, false, false)

cmd/limactl/start.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/spf13/pflag"
1818

1919
"github.com/lima-vm/lima/v2/cmd/limactl/editflags"
20+
"github.com/lima-vm/lima/v2/pkg/autostart"
2021
"github.com/lima-vm/lima/v2/pkg/driverutil"
2122
"github.com/lima-vm/lima/v2/pkg/editutil"
2223
"github.com/lima-vm/lima/v2/pkg/instance"
@@ -580,9 +581,14 @@ func startAction(cmd *cobra.Command, args []string) error {
580581
logrus.Warnf("expected status %q, got %q", limatype.StatusStopped, inst.Status)
581582
}
582583
ctx := cmd.Context()
583-
err = networks.Reconcile(ctx, inst.Name)
584-
if err != nil {
585-
return err
584+
// Network reconciliation will be performed by the process launched by the autostart manager
585+
if registered, err := autostart.IsRegistered(ctx, inst); err != nil && !errors.Is(err, autostart.ErrNotSupported) {
586+
return fmt.Errorf("failed to check if the autostart entry for instance %q is registered: %w", inst.Name, err)
587+
} else if !registered {
588+
err = networks.Reconcile(ctx, inst.Name)
589+
if err != nil {
590+
return err
591+
}
586592
}
587593

588594
launchHostAgentForeground := false

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515
github.com/containerd/continuity v0.4.5
1616
github.com/containers/gvisor-tap-vsock v0.8.7 // gomodjail:unconfined
1717
github.com/coreos/go-semver v0.3.1
18+
github.com/coreos/go-systemd/v22 v22.5.0
1819
github.com/cpuguy83/go-md2man/v2 v2.0.7
1920
github.com/digitalocean/go-qemu v0.0.0-20221209210016-f035778c97f7
2021
github.com/diskfs/go-diskfs v1.7.0 // gomodjail:unconfined

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ github.com/containers/gvisor-tap-vsock v0.8.7 h1:mFMMU5CIXO9sbtsgECc90loUHx15km3
4444
github.com/containers/gvisor-tap-vsock v0.8.7/go.mod h1:Rf2gm4Lpac0IZbg8wwQDh7UuKCxHmnxar0hEZ08OXY8=
4545
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
4646
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
47+
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
48+
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
4749
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
4850
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
4951
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -104,6 +106,7 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
104106
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
105107
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
106108
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
109+
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
107110
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
108111
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
109112
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=

pkg/autostart/autostart.go

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"runtime"
1010
"sync"
1111

12+
"github.com/lima-vm/lima/v2/pkg/autostart/systemd"
1213
"github.com/lima-vm/lima/v2/pkg/limatype"
1314
)
1415

@@ -27,20 +28,46 @@ func UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) er
2728
return manager().UnregisterFromStartAtLogin(ctx, inst)
2829
}
2930

31+
// AutoStartedIdentifier returns the identifier if the current process was started by the autostart manager.
32+
func AutoStartedIdentifier() string {
33+
return manager().AutoStartedIdentifier()
34+
}
35+
36+
// RequestStart requests to start the instance by identifier.
37+
func RequestStart(ctx context.Context, inst *limatype.Instance) error {
38+
return manager().RequestStart(ctx, inst)
39+
}
40+
41+
// RequestStop requests to stop the instance by identifier.
42+
func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {
43+
return manager().RequestStop(ctx, inst)
44+
}
45+
3046
type autoStartManager interface {
3147
// Registration
3248
IsRegistered(ctx context.Context, inst *limatype.Instance) (bool, error)
3349
RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error
3450
UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error
51+
52+
// Status
53+
AutoStartedIdentifier() string
54+
55+
// Operation
56+
// RequestStart requests to start the instance by identifier.
57+
RequestStart(ctx context.Context, inst *limatype.Instance) error
58+
// RequestStop requests to stop the instance by identifier.
59+
RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error)
3560
}
3661

3762
var manager = sync.OnceValue(func() autoStartManager {
3863
switch runtime.GOOS {
3964
case "darwin":
4065
return Launchd
4166
case "linux":
42-
return Systemd
43-
default:
44-
return &notSupportedManager{}
67+
if systemd.IsRunningSystemd() {
68+
return Systemd
69+
}
70+
// TODO: support other init systems
4571
}
72+
return &notSupportedManager{}
4673
})

pkg/autostart/autostart_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func TestRenderTemplate(t *testing.T) {
3838
<string>start</string>
3939
<string>default</string>
4040
<string>--foreground</string>
41+
<string>--progress</string>
4142
</array>
4243
<key>RunAtLoad</key>
4344
<true/>
@@ -65,7 +66,7 @@ Description=Lima - Linux virtual machines, with a focus on running containers.
6566
Documentation=man:lima(1)
6667
6768
[Service]
68-
ExecStart=/limactl start %i --foreground
69+
ExecStart=/limactl start %i --foreground --progress
6970
WorkingDirectory=%h
7071
Type=simple
7172
TimeoutSec=10

pkg/autostart/launchd/io.lima-vm.autostart.INSTANCE.plist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<string>start</string>
1111
<string>{{ .Instance }}</string>
1212
<string>--foreground</string>
13+
<string>--progress</string>
1314
</array>
1415
<key>RunAtLoad</key>
1516
<true/>

pkg/autostart/launchd/launchd.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import (
99
"fmt"
1010
"os"
1111
"os/exec"
12+
"sync"
1213

1314
"github.com/sirupsen/logrus"
15+
16+
"github.com/lima-vm/lima/v2/pkg/limatype"
1417
)
1518

1619
//go:embed io.lima-vm.autostart.INSTANCE.plist
@@ -32,7 +35,7 @@ func EnableDisableService(ctx context.Context, enable bool, instName string) err
3235
if !enable {
3336
action = "disable"
3437
}
35-
return launchctl(ctx, action, fmt.Sprintf("gui/%d/%s", os.Getuid(), ServiceNameFrom(instName)))
38+
return launchctl(ctx, action, serviceTarget(instName))
3639
}
3740

3841
func launchctl(ctx context.Context, args ...string) error {
@@ -42,3 +45,44 @@ func launchctl(ctx context.Context, args ...string) error {
4245
logrus.Debugf("running command: %v", cmd.Args)
4346
return cmd.Run()
4447
}
48+
49+
// AutoStartedServiceName returns the launchd service name if the instance is started by launchd.
50+
func AutoStartedServiceName() string {
51+
// Assume the instance is started by launchd if XPC_SERVICE_NAME is set and not "0".
52+
// To confirm it is actually started by launchd, it needs to use `launch_activate_socket`.
53+
// But that requires actual socket activation setup in the plist file.
54+
// So we just check XPC_SERVICE_NAME here.
55+
if xpcServiceName := os.Getenv("XPC_SERVICE_NAME"); xpcServiceName != "0" {
56+
return xpcServiceName
57+
}
58+
return ""
59+
}
60+
61+
var domainTarget = sync.OnceValue(func() string {
62+
return fmt.Sprintf("gui/%d", os.Getuid())
63+
})
64+
65+
func serviceTarget(instName string) string {
66+
return fmt.Sprintf("%s/%s", domainTarget(), ServiceNameFrom(instName))
67+
}
68+
69+
func RequestStart(ctx context.Context, inst *limatype.Instance) error {
70+
// If disabled, bootstrap will fail.
71+
_ = EnableDisableService(ctx, true, inst.Name)
72+
if err := launchctl(ctx, "bootstrap", domainTarget(), GetPlistPath(inst.Name)); err != nil {
73+
return fmt.Errorf("failed to start the instance %q via launchctl: %w", inst.Name, err)
74+
}
75+
return nil
76+
}
77+
78+
func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) {
79+
logrus.Debugf("AutoStartedIdentifier=%q, ServiceNameFrom=%q", inst.AutoStartedIdentifier, ServiceNameFrom(inst.Name))
80+
if inst.AutoStartedIdentifier == ServiceNameFrom(inst.Name) {
81+
logrus.Infof("Stopping the instance %q started by launchd", inst.Name)
82+
if err := launchctl(ctx, "bootout", serviceTarget(inst.Name)); err != nil {
83+
return false, fmt.Errorf("failed to stop the instance %q via launchctl: %w", inst.Name, err)
84+
}
85+
return true, nil
86+
}
87+
return false, nil
88+
}

0 commit comments

Comments
 (0)