chultun/internal/budget/units.go

179 lines
4.6 KiB
Go

package budget
import (
"fmt"
"slices"
"strings"
)
const serviceText = "[Unit]\nDescription=Terminal workload resource profiles\n\n[Service]\nExecStart=%h/.local/bin/budget watch\n\n[Install]\nWantedBy=default.target\n"
func escape(name string) string {
var result strings.Builder
for i := range len(name) {
b := name[i]
switch {
case b == '/':
result.WriteByte('-')
case b >= 'a' && b <= 'z', b >= 'A' && b <= 'Z', b >= '0' && b <= '9', b == ':', b == '_', b == '.' && i != 0:
result.WriteByte(b)
default:
fmt.Fprintf(&result, `\x%02x`, b)
}
}
return result.String()
}
func appDirs(app string) []string {
app = escape(app)
return []string{"app-" + app + ".service.d", "app-" + app + "@.service.d", "app-" + app + "-.scope.d", "app-flatpak-" + app + "-.scope.d"}
}
func appMatches(app, unit string) bool {
app = escape(app)
return unit == "app-"+app+".service" || strings.HasPrefix(unit, "app-"+app+"@") || strings.HasPrefix(unit, "app-"+app+"-") || strings.HasPrefix(unit, "app-flatpak-"+app+"-")
}
func (c config) sharedSlice() string {
apps := keys(c.Shared)
if len(apps) == 0 {
return ""
}
return "app-" + escape(apps[0]) + ".slice"
}
func (c config) unitProfile(unit string) string {
for _, name := range keys(c.Units) {
if slices.Contains(c.Units[name], unit) {
return name
}
}
for _, app := range keys(c.Shared) {
if appMatches(app, unit) {
return c.Shared[app]
}
}
for _, name := range keys(c.Apps) {
for _, app := range c.Apps[name] {
if appMatches(app, unit) {
return name
}
}
}
for _, name := range keys(c.Prefixes) {
for _, prefix := range c.Prefixes[name] {
if prefixMatches(prefix, unit) {
return name
}
}
}
if unit == "search.slice" {
return "search"
}
if slice := c.sharedSlice(); slice != "" && unit == slice {
return "default"
}
if strings.HasPrefix(unit, "app-") && (strings.HasSuffix(unit, ".scope") || strings.HasSuffix(unit, ".service")) {
return "default"
}
return ""
}
func (c config) activeProfile(u unit) string {
if name := c.unitProfile(u.Name); name != "" {
return name
}
if strings.HasPrefix(u.Name, "budget-") && strings.HasSuffix(u.Name, ".scope") {
if name, ok := strings.CutSuffix(u.Description, " workload"); ok && name != "search" {
return name
}
}
return ""
}
func prefixMatches(directory, unit string) bool {
for _, kind := range []string{".service", ".scope", ".slice"} {
if stem, ok := strings.CutSuffix(directory, kind+".d"); ok {
return strings.HasPrefix(unit, stem) && strings.HasSuffix(unit, kind)
}
}
return false
}
func dropSection(directory string) (string, error) {
for _, kind := range []struct{ suffix, section string }{
{".service.d", "Service"}, {".scope.d", "Scope"}, {".slice.d", "Slice"},
} {
if stem, ok := strings.CutSuffix(directory, kind.suffix); ok && stem != "" {
return kind.section, nil
}
}
return "", fmt.Errorf("invalid drop-in directory %q", directory)
}
func (c config) render() (map[string]string, error) {
if err := c.validate(); err != nil {
return nil, err
}
files := map[string]string{}
add := func(directory, name, extra string) error {
values, err := c.assignments(name)
if err != nil {
return err
}
section, err := dropSection(directory)
if err != nil {
return err
}
files[directory+"/50-budget.conf"] = "[" + section + "]\n" + strings.Join(values, "\n") + "\n" + extra
return nil
}
for _, directory := range []string{"app-.service.d", "app-.scope.d"} {
if err := add(directory, "default", ""); err != nil {
return nil, err
}
}
for _, name := range keys(c.Apps) {
for _, app := range c.Apps[name] {
for _, directory := range appDirs(app) {
if err := add(directory, name, ""); err != nil {
return nil, err
}
}
}
}
for _, name := range keys(c.Units) {
for _, unit := range c.Units[name] {
if err := add(unit+".d", name, ""); err != nil {
return nil, err
}
}
}
for _, name := range keys(c.Prefixes) {
for _, directory := range c.Prefixes[name] {
if err := add(directory, name, ""); err != nil {
return nil, err
}
}
}
for _, app := range keys(c.Shared) {
for _, directory := range appDirs(app) {
if err := add(directory, c.Shared[app], "Slice="+c.sharedSlice()+"\n"); err != nil {
return nil, err
}
}
}
groups := map[string]string{"search.slice": "search"}
if len(c.Shared) > 0 {
groups[c.sharedSlice()] = "default"
}
for unit, name := range groups {
values, err := c.assignments(name)
if err != nil {
return nil, err
}
files[unit] = "[Slice]\n" + strings.Join(values, "\n") + "\n"
}
files["budget.service"] = serviceText
return files, nil
}