package hostsvc import ( "cmp" "errors" "fmt" "path/filepath" "strconv" "strings" "github.com/johnandersen777/socialweb-computer-kcp/internal/systemdunit" ) const ( ArgsVar = "SWC_ARGS" WantedBy = "default.target" EnvFileSuffix = ".env" ) type InstalledFile struct { Unit []byte Env []byte EnvPath string } type InstallOptions struct { Exec string EnvFile string } func quoteArg(a string) (string, error) { if a == "" || strings.ContainsAny(a, " \t\n\"'\\$%;") { return "", fmt.Errorf("hostsvc: argument %q cannot travel through an EnvironmentFile word list", a) } return a, nil } func signalName(s int) string { switch s { case 2: return "SIGINT" case 9: return "SIGKILL" case 15: return "SIGTERM" case 3: return "SIGQUIT" } return strconv.Itoa(s) } func RenderInstalled(spec UnitSpec, o InstallOptions) (InstalledFile, error) { if err := ValidateSpec(spec); err != nil { return InstalledFile{}, err } if len(spec.Exec) == 0 { return InstalledFile{}, errors.New("hostsvc: an installed unit needs an executable") } exe := o.Exec if exe == "" { exe = spec.Exec[0] } if !filepath.IsAbs(exe) || !filepath.IsAbs(o.EnvFile) { return InstalledFile{}, fmt.Errorf("hostsvc: %s: the executable and the environment file must be absolute paths", spec.Name) } args := make([]string, 0, len(spec.Exec)-1) for _, a := range spec.Exec[1:] { q, err := quoteArg(a) if err != nil { return InstalledFile{}, fmt.Errorf("%s: %w", spec.Name, err) } args = append(args, q) } desc := spec.Description desc = cmp.Or(desc, spec.Name) slice := spec.Slice slice = cmp.Or(slice, DefaultSlice) restart := spec.Restart restart = cmp.Or(restart, RestartNo) oom := spec.OOMPolicy oom = cmp.Or(oom, "stop") var unitEntries []systemdunit.KeyValue add := func(k, v string) { unitEntries = append(unitEntries, systemdunit.KV(k, v)) } add("Description", desc) if len(spec.After) > 0 { add("After", strings.Join(spec.After, " ")) } if len(spec.Wants) > 0 { add("Wants", strings.Join(spec.Wants, " ")) } add("StartLimitIntervalSec", "0") service := []systemdunit.KeyValue{ systemdunit.KV("Type", "exec"), systemdunit.KV("Slice", slice), systemdunit.KV("KillMode", "control-group"), systemdunit.KV("OOMPolicy", oom), systemdunit.KV("Restart", string(restart)), } if restart != RestartNo { service = append(service, systemdunit.KV("RestartSec", "2")) } if spec.WorkDir != "" { service = append(service, systemdunit.KV("WorkingDirectory", spec.WorkDir)) } for _, e := range spec.Env { q, err := quoteArg(e) if err != nil { return InstalledFile{}, fmt.Errorf("%s: %w", spec.Name, err) } service = append(service, systemdunit.KV("Environment", q)) } service = append(service, systemdunit.KV("EnvironmentFile", o.EnvFile), systemdunit.KV("ExecStart", exe+" $"+ArgsVar)) if spec.KillSignal != 0 { service = append(service, systemdunit.KV("KillSignal", signalName(int(spec.KillSignal)))) } if spec.StopTimeout > 0 { service = append(service, systemdunit.KV("TimeoutStopSec", strconv.FormatInt(int64(spec.StopTimeout.Seconds()), 10))) } if spec.Output.StdoutFile != "" { service = append(service, systemdunit.KV("StandardOutput", "append:"+spec.Output.StdoutFile)) } if spec.Output.StderrFile != "" { service = append(service, systemdunit.KV("StandardError", "append:"+spec.Output.StderrFile)) } if spec.Limits.MemoryMax > 0 { service = append(service, systemdunit.KV("MemoryMax", strconv.FormatUint(spec.Limits.MemoryMax, 10)), systemdunit.KV("MemorySwapMax", "0")) } if spec.Delegate { service = append(service, systemdunit.KV("Delegate", "yes")) } if spec.Limits.TasksMax > 0 { service = append(service, systemdunit.KV("TasksMax", strconv.FormatUint(spec.Limits.TasksMax, 10))) } if spec.Limits.CPUWeight > 0 { service = append(service, systemdunit.KV("CPUWeight", strconv.FormatUint(spec.Limits.CPUWeight, 10))) } if spec.FDStore > 0 { service = append(service, systemdunit.KV("FileDescriptorStoreMax", strconv.Itoa(spec.FDStore)), systemdunit.KV("NotifyAccess", "main")) } unit := systemdunit.Unit{Sections: []systemdunit.Section{ {Name: "Unit", Entries: unitEntries}, {Name: "Service", Entries: service}, {Name: "Install", Entries: []systemdunit.KeyValue{systemdunit.KV("WantedBy", WantedBy)}}, }} env := ArgsVar + "=" + strings.Join(args, " ") + "\n" return InstalledFile{Unit: unit.Render(), Env: []byte(env), EnvPath: o.EnvFile}, nil } func RenderDropIn(after, wants []string) []byte { var entries []systemdunit.KeyValue if len(after) > 0 { entries = append(entries, systemdunit.KV("After", strings.Join(after, " "))) } if len(wants) > 0 { entries = append(entries, systemdunit.KV("Wants", strings.Join(wants, " "))) } return systemdunit.Unit{Sections: []systemdunit.Section{{Name: "Unit", Entries: entries}}}.Render() } func ParseArgsEnv(b []byte) ([]string, error) { for l := range strings.SplitSeq(string(b), "\n") { if v, ok := strings.CutPrefix(l, ArgsVar+"="); ok { return strings.Fields(v), nil } } return nil, fmt.Errorf("hostsvc: no %s line", ArgsVar) }