-
Notifications
You must be signed in to change notification settings - Fork 719
autostart: Ensure instance is started/stopped by launchd or systemctl
#4139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d9a122b
45d7799
ff3f3a6
0e11d6e
3eb1197
1613e09
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 { | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The linter rule should be subject to |
||||||||
| 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) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,118 +6,55 @@ package autostart | |
|
|
||
| import ( | ||
| "context" | ||
| _ "embed" | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path" | ||
| "path/filepath" | ||
| "strconv" | ||
| "strings" | ||
| "sync" | ||
|
|
||
| "github.com/lima-vm/lima/v2/pkg/textutil" | ||
| "github.com/lima-vm/lima/v2/pkg/limatype" | ||
| ) | ||
|
|
||
| //go:embed [email protected] | ||
| var systemdTemplate string | ||
|
|
||
| //go:embed io.lima-vm.autostart.INSTANCE.plist | ||
| var launchdTemplate 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) | ||
| } | ||
|
|
||
| // 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)) | ||
| // RegisterToStartAtLogin creates a start-at-login entry for the instance. | ||
| func RegisterToStartAtLogin(ctx context.Context, inst *limatype.Instance) error { | ||
| return manager().RegisterToStartAtLogin(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 | ||
| // UnregisterFromStartAtLogin deletes the start-at-login entry for the instance. | ||
| func UnregisterFromStartAtLogin(ctx context.Context, inst *limatype.Instance) error { | ||
| return manager().UnregisterFromStartAtLogin(ctx, inst) | ||
| } | ||
|
|
||
| // 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 | ||
| // AutoStartedIdentifier returns the identifier if the current process was started by the autostart manager. | ||
| func AutoStartedIdentifier() string { | ||
| return manager().AutoStartedIdentifier() | ||
| } | ||
|
|
||
| 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))) | ||
| // RequestStart requests to start the instance by identifier. | ||
| func RequestStart(ctx context.Context, inst *limatype.Instance) error { | ||
| return manager().RequestStart(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() | ||
| // RequestStop requests to stop the instance by identifier. | ||
| func RequestStop(ctx context.Context, inst *limatype.Instance) (bool, error) { | ||
| return manager().RequestStop(ctx, inst) | ||
| } | ||
|
|
||
| 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 | ||
| } | ||
| return textutil.ExecuteTemplate( | ||
| tmpToExecute, | ||
| map[string]string{ | ||
| "Binary": selfExeAbs, | ||
| "Instance": instName, | ||
| "WorkDir": workDir, | ||
| }) | ||
| 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) | ||
| } | ||
|
|
||
| var manager = sync.OnceValue(Manager) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic seems to be repeated in several places. Should it be part of
reconcile.Reconcile()instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are several
reconcile.Reconcile()calls that are not related toautostart.IsRegistered().It is not good to put it in
reconcile.Reconcile().