blob: 569efa8aa0956e15c97d59b3a0ef224cad78c8f8 [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cgroups
import (
"errors"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
func findSystemdRun() (string, error) {
bin, err := exec.LookPath("systemd-run")
if errors.Is(err, exec.ErrNotFound) {
return "", fmt.Errorf("systemd-run binary not found")
} else if err != nil {
return "", fmt.Errorf("error looking for systemd-run: %w", err)
}
scope := fmt.Sprintf("systemd-run-test-%d.scope", time.Now().UnixNano())
cmd := exec.Command(bin, "--user", "--scope", "--unit", scope, "/bin/true")
sout, serr := cmd.CombinedOutput()
if serr == nil {
// It works!
return bin, nil
}
var context strings.Builder
fmt.Fprintf(&context, "\noutput: %s", string(sout))
// Failed. systemd-run probably just said to look at journalctl;
// collect that additional context.
cmd = exec.Command("journalctl", "--catalog", "--user", "--unit", scope)
jout, jerr := cmd.CombinedOutput()
if jerr != nil {
fmt.Fprintf(&context, "\njournalctl error: %v\noutout: %s", jerr, string(jout))
} else {
fmt.Fprintf(&context, "\njournalctl output: %s", string(jout))
}
// Attempt to cleanup unit.
cmd = exec.Command("systemctl", "--user", "reset-failed", scope)
scout, scerr := cmd.CombinedOutput()
if scerr != nil {
fmt.Fprintf(&context, "\nsystemctl cleanup error: %v\noutput: %s", scerr, string(scout))
}
return "", fmt.Errorf("system-run failed: %w%s", serr, context.String())
}
var (
systemdOnce sync.Once
systemdRunPath string
systemdRunError error
)
type Cmd struct {
exec.Cmd
modified bool
path string
scope string
}
func WrapCommand(cmd *exec.Cmd, scope string) (*Cmd, error) {
wrapped := Cmd{Cmd: *cmd}
systemdOnce.Do(func() {
systemdRunPath, systemdRunError = findSystemdRun()
})
if systemdRunError != nil {
fmt.Fprintf(os.Stderr, "# warning: systemd-run not available: %v\n# skipping cgroup wrapper...\n", systemdRunError)
return &wrapped, nil
}
u, err := user.Current()
if err != nil {
return nil, err
}
wrapped.Cmd.Args = append([]string{
systemdRunPath, "--user", "--scope", "--unit=" + scope,
}, wrapped.Cmd.Args...)
wrapped.Cmd.Path = systemdRunPath
wrapped.modified = true
wrapped.path = fmt.Sprintf("user-%s.slice/user@%s.service/app.slice", u.Uid, u.Uid)
wrapped.scope = scope
return &wrapped, nil
}
func (c *Cmd) RSSFunc() func() (uint64, error) {
if !c.modified {
return nil
}
memPath := filepath.Join("/sys/fs/cgroup/user.slice", c.path, c.scope, "memory.current")
return func() (uint64, error) {
data, err := os.ReadFile(memPath)
if err != nil {
return 0, err
}
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
}
}