Skip to content
quarry Artifact [4a0bfae274]
Login

Artifact [4a0bfae274]

Artifact 4a0bfae274a5f5e163ac7018bd40899b27ca5c33005f2f0aa1bbb8126970c7d8:


package runner

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
)

// Per-task temp directories (#317). A task killed before `go test`
// removed its go-build directories left them in the host's /tmp, a
// tmpfs that filled until NATS refused streams. Each task now gets a
// directory of its own under the runner's TempRoot, handed to it as
// TMPDIR and GOTMPDIR, and the runner removes that directory when the
// task ends, after reclaim has ended whatever the task left running.

const (
	// tempRootName is the default root's name under WorkDir, one no
	// other program has a reason to use.
	tempRootName = "quarry-runner-tmp"
	// taskPrefix starts every task dir's name, followed by the runner's
	// pid, so a sweep knows its own dirs and which runner owns each.
	taskPrefix = "quarry-task-"
)

// sweepTemp makes the runner's temp root and removes the task dirs a
// previous life of this runner left: those named taskPrefix<pid>-*
// whose pid is this process's (a restart in a container is pid 1 again)
// or no longer runs, so another runner sharing the work dir keeps its
// live ones. Every other entry is left alone, and a root that is a
// symlink or not a directory is refused, never followed.
func (r *Runner) sweepTemp() error {
	root := r.cfg.TempRoot
	if err := os.MkdirAll(filepath.Dir(root), 0o755); err != nil {
		return err
	}
	if err := os.Mkdir(root, 0o700); err != nil && !os.IsExist(err) {
		return err
	}
	info, err := os.Lstat(root)
	if err != nil {
		return err
	}
	if !info.IsDir() {
		return fmt.Errorf("runner: temp root %s is a %s, not a directory", root, info.Mode().Type())
	}
	if r.users != nil {
		// A job user must be able to reach its own directory under it.
		if err := os.Chmod(root, 0o711); err != nil {
			return err
		}
	}
	es, err := os.ReadDir(root)
	if err != nil {
		return err
	}
	for _, e := range es {
		pid, ok := taskPid(e.Name())
		if !ok || !e.IsDir() || (pid != os.Getpid() && pidAlive(pid)) {
			continue
		}
		if err := removeAll(filepath.Join(root, e.Name())); err != nil {
			r.cfg.Logf("stale temp dir %s was not removed: %v", e.Name(), err)
		}
	}
	return nil
}

// taskPid reads the pid out of a task dir's name.
func taskPid(name string) (int, bool) {
	rest, ok := strings.CutPrefix(name, taskPrefix)
	if !ok {
		return 0, false
	}
	n, _, _ := strings.Cut(rest, "-")
	pid, err := strconv.Atoi(n)
	return pid, err == nil && pid > 0
}

// taskTemp makes one task's temp directory.
func (r *Runner) taskTemp() (string, error) {
	if err := os.MkdirAll(r.cfg.TempRoot, 0o700); err != nil {
		return "", err
	}
	return os.MkdirTemp(r.cfg.TempRoot, taskPrefix+strconv.Itoa(os.Getpid())+"-")
}

// withTemp points every temp variable a command reads at tmp. It comes
// after whatever the task set, so the per-task directory overrides a
// TMPDIR in a job's or step's env: a step cannot send its scratch space
// back to the host's /tmp.
func withTemp(next starter, tmp string) starter {
	temp := map[string]string{"TMPDIR": tmp, "GOTMPDIR": tmp, "TMP": tmp, "TEMP": tmp}
	return func(ctx context.Context, dir string, argv []string, env ...map[string]string) *exec.Cmd {
		return next(ctx, dir, argv, append(env, temp)...)
	}
}

// removeAll removes dir, making read-only directories writable first
// where the plain removal is refused.
func removeAll(dir string) error {
	err := os.RemoveAll(dir)
	if err == nil {
		return nil
	}
	filepath.WalkDir(dir, func(p string, d os.DirEntry, _ error) error {
		if d != nil && d.IsDir() {
			os.Chmod(p, 0o700)
		}
		return nil
	})
	return os.RemoveAll(dir)
}