2026-10-05 19:19:05 +09:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-05 21:45:05 +09:00
|
|
|
for _, app := range keys(c.Shared) {
|
|
|
|
|
if appMatches(app, unit) {
|
|
|
|
|
return c.Shared[app]
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-05 19:19:05 +09:00
|
|
|
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] {
|
2026-10-05 21:45:05 +09:00
|
|
|
if prefixMatches(prefix, unit) {
|
2026-10-05 19:19:05 +09:00
|
|
|
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 ""
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 21:45:05 +09:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-05 19:19:05 +09:00
|
|
|
func (c config) render() (map[string]string, error) {
|
2026-10-05 21:45:05 +09:00
|
|
|
if err := c.validate(); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-10-05 19:19:05 +09:00
|
|
|
files := map[string]string{}
|
|
|
|
|
add := func(directory, name, extra string) error {
|
|
|
|
|
values, err := c.assignments(name)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-10-05 21:45:05 +09:00
|
|
|
section, err := dropSection(directory)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
2026-10-05 19:19:05 +09:00
|
|
|
}
|
|
|
|
|
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
|
|
|
|
|
}
|