//go:build linux package accounts import ( "bytes" "context" "errors" "fmt" "os" "os/exec" "runtime" "strings" "syscall" "golang.org/x/sys/unix" ) // ioprioIdle is the idle I/O class (IOPRIO_CLASS_IDLE, 3, shifted by IOPRIO_CLASS_SHIFT, 13): the process gets // the disk only when nothing else asks for it, where the disk's scheduler honours classes (BFQ does). const ioprioIdle = 3 << 13 // Idle runs a command as CLocale does, at the lowest priority the machine has (novox/hq issue 361): CPU nice // 19 and the idle I/O class, set on the process as soon as it has started — so the search for setuid programs, // which walks every file and runs as long as that takes, yields to everything else. It dies with the engine // (the parent-death signal), so a restarted engine never leaves a walk running beside its own. A priority it // could not lower is a failure: the command is killed rather than walk the disks at full priority. func Idle(ctx context.Context, name string, args ...string) (string, error) { cmd := exec.CommandContext(ctx, name, args...) cmd.Env = append(os.Environ(), "LC_ALL=C", "LANG=C", "LANGUAGE=C") cmd.SysProcAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGKILL} var stdout, stderr bytes.Buffer cmd.Stdout, cmd.Stderr = &stdout, &stderr // The parent-death signal follows the thread that started the child, not the process: held on this // thread until the child has ended, so the runtime retiring a thread never kills a walk midway. runtime.LockOSThread() defer runtime.UnlockOSThread() if err := cmd.Start(); err != nil { return "", fmt.Errorf("%s: %w", name, err) } pid := cmd.Process.Pid if err := lower(pid); err != nil { _ = cmd.Process.Kill() _ = cmd.Wait() return "", fmt.Errorf("%s: its priority could not be lowered, so it was stopped: %w", name, err) } err := cmd.Wait() out := stdout.String() if err != nil { var exit *exec.ExitError if errors.As(err, &exit) { return out, fmt.Errorf("%s exited %d: %s", name, exit.ExitCode(), strings.TrimSpace(stderr.String())) } return out, fmt.Errorf("%s: %w", name, err) } return out, nil } // lower sets a process's CPU nice to 19 and its I/O class to idle. func lower(pid int) error { if err := unix.Setpriority(unix.PRIO_PROCESS, pid, 19); err != nil { return fmt.Errorf("nice: %w", err) } if _, _, errno := unix.Syscall(unix.SYS_IOPRIO_SET, 1 /* IOPRIO_WHO_PROCESS */, uintptr(pid), ioprioIdle); errno != 0 { return fmt.Errorf("I/O class: %w", errno) } return nil } // LowerThisThread puts the calling goroutine on a thread of its own at nice 19 and the idle I/O class, for the // walk for setuid programs, which runs in the engine (novox/hq issue 361): on Linux both are a thread's, so the // engine's other work keeps its priority. The goroutine is locked to the thread and never unlocked, so the // runtime ends the thread with it rather than hand a lowered thread to other work; a command it starts inherits // the priority too. func LowerThisThread() error { runtime.LockOSThread() return lower(unix.Gettid()) }