package systemd import ( "fmt" "strings" "text/template" "time" "github.com/warkanum/unitdore/config" "github.com/warkanum/unitdore/service" ) const systemUnitTemplate = `# /etc/systemd/system/{{.ServiceName}} # Generated by unitdore — do not edit manually [Unit] Description=Unitdore: {{.Unit.Name}} ({{.Label}}) After=network.target{{with .RuntimeService}} {{.}}{{end}}{{range .After}} {{.}}{{end}}{{with .RuntimeService}} Requires={{.}}{{end}} [Service] Type={{.ServiceType}} {{- if eq .ServiceType "oneshot"}} RemainAfterExit=yes {{- end}} {{- with .WorkingDir}} WorkingDirectory={{.}} {{- end}} {{- with .DelaySec}} ExecStartPre=/usr/bin/env sleep {{.}} TimeoutStartSec={{$.StartTimeout}} {{- end}} ExecStart={{.ExecStart}} ExecStop={{if .ExecStop}}-{{.ExecStop}}{{end}} {{- if .Unit.Restart}} Restart=on-failure RestartSec={{.Unit.RestartSec}} {{- if gt .Unit.RestartRetries 0}} StartLimitBurst={{.Unit.RestartRetries}} {{- end}} {{- end}} [Install] WantedBy=multi-user.target ` const userUnitTemplate = `# ~/.config/systemd/user/{{.ServiceName}} # Generated by unitdore — do not edit manually [Unit] Description=Unitdore: {{.Unit.Name}} ({{.Label}}) After=default.target{{range .After}} {{.}}{{end}} [Service] Type={{.ServiceType}} {{- if eq .ServiceType "oneshot"}} RemainAfterExit=yes {{- end}} {{- with .WorkingDir}} WorkingDirectory={{.}} {{- end}} {{- with .DelaySec}} ExecStartPre=/usr/bin/env sleep {{.}} TimeoutStartSec={{$.StartTimeout}} {{- end}} ExecStart={{.ExecStart}} ExecStop={{if .ExecStop}}-{{.ExecStop}}{{end}} {{- if .Unit.Restart}} Restart=on-failure RestartSec={{.Unit.RestartSec}} {{- if gt .Unit.RestartRetries 0}} StartLimitBurst={{.Unit.RestartRetries}} {{- end}} {{- end}} [Install] WantedBy=default.target ` type templateData struct { ServiceName string Unit config.Unit ExecStart string ExecStop string RuntimeService string Label string // shown in Description ServiceType string // simple | oneshot WorkingDir string After []string // service names of the previous order group DelaySec int // ExecStartPre sleep, from unit delay StartTimeout int // TimeoutStartSec when a delay is set } // ServiceName returns the systemd service name for a unit, with optional prefix/suffix. func ServiceName(u config.Unit, prefix, suffix string) string { return service.BaseName(u, prefix, suffix) + ".service" } // Generate produces the .service file content for a unit. func Generate(u config.Unit, prefix, suffix string) (string, error) { if err := service.Validate(u); err != nil { return "", err } spec := service.Resolve(u) data := templateData{ ServiceName: ServiceName(u, prefix, suffix), Unit: u, ExecStart: spec.Start, ExecStop: spec.Stop, RuntimeService: runtimeService(u.Runtime), Label: spec.Label, ServiceType: spec.Type, WorkingDir: spec.WorkingDir, } for _, n := range u.After { data.After = append(data.After, ServiceName(config.Unit{Name: n}, prefix, suffix)) } if u.StartDelay > 0 { data.DelaySec = int((u.StartDelay + time.Second - 1) / time.Second) data.StartTimeout = data.DelaySec + 90 } tmplStr := systemUnitTemplate if u.User != "" { tmplStr = userUnitTemplate } tmpl, err := template.New("unit").Parse(tmplStr) if err != nil { return "", fmt.Errorf("parsing template: %w", err) } var buf strings.Builder if err := tmpl.Execute(&buf, data); err != nil { return "", fmt.Errorf("rendering template: %w", err) } return buf.String(), nil } // runtimeService returns the systemd service name that must be running for the // given container runtime, or "" if none is required (e.g. daemonless podman). func runtimeService(runtime string) string { if runtime == "docker" { return "docker.service" } return "" }