Update go version
This commit is contained in:
10
vendor/honnef.co/go/tools/lintcmd/cache/UPSTREAM
vendored
Normal file
10
vendor/honnef.co/go/tools/lintcmd/cache/UPSTREAM
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
This package is a copy of cmd/go/internal/cache.
|
||||
|
||||
Differences from upstream:
|
||||
- we continue to use renameio instead of lockedfile for writing trim.txt
|
||||
- we still use I/O helpers that work with earlier versions of Go.
|
||||
- we use a cache directory specific to Staticcheck
|
||||
- we use a Staticcheck-specific salt
|
||||
|
||||
The last upstream commit we've looked at was:
|
||||
06ac303f6a14b133254f757e54599c48e3c2a4ad
|
||||
566
vendor/honnef.co/go/tools/lintcmd/cache/cache.go
vendored
Normal file
566
vendor/honnef.co/go/tools/lintcmd/cache/cache.go
vendored
Normal file
@@ -0,0 +1,566 @@
|
||||
// Copyright 2017 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 cache implements a build artifact cache.
|
||||
//
|
||||
// This package is a slightly modified fork of Go's
|
||||
// cmd/go/internal/cache package.
|
||||
package cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"honnef.co/go/tools/internal/renameio"
|
||||
)
|
||||
|
||||
// An ActionID is a cache action key, the hash of a complete description of a
|
||||
// repeatable computation (command line, environment variables,
|
||||
// input file contents, executable contents).
|
||||
type ActionID [HashSize]byte
|
||||
|
||||
// An OutputID is a cache output key, the hash of an output of a computation.
|
||||
type OutputID [HashSize]byte
|
||||
|
||||
// Cache is the interface for a build artifact cache.
|
||||
type Cache interface {
|
||||
// Get returns the cache entry for the provided ActionID.
|
||||
// On miss, the error type should be of type *entryNotFoundError.
|
||||
Get(ActionID) (Entry, error)
|
||||
|
||||
// Put adds an item to the cache. The seeker is only used to seek to
|
||||
// the beginning. After a call to Put, the seek position is not
|
||||
// guaranteed to be in any particular state.
|
||||
//
|
||||
// As a special case, if the ReadSeeker is of type noVerifyReadSeeker,
|
||||
// the verification from GODEBUG=gocacheverify=1 is skipped.
|
||||
Put(ActionID, io.ReadSeeker) (OutputID, int64, error)
|
||||
|
||||
// Close closes the cache and releases any resources. For DiskCache
|
||||
// this trims old entries.
|
||||
Close() error
|
||||
|
||||
// OutputFile returns the path to the on-disk file for the given OutputID.
|
||||
OutputFile(OutputID) string
|
||||
}
|
||||
|
||||
// A DiskCache is a package cache backed by a file system directory tree.
|
||||
type DiskCache struct {
|
||||
dir string
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// Open opens and returns the cache in the given directory.
|
||||
//
|
||||
// It is safe for multiple processes on a single machine to use the
|
||||
// same cache directory in a local file system simultaneously.
|
||||
// They will coordinate using operating system file locks and may
|
||||
// duplicate effort but will not corrupt the cache.
|
||||
//
|
||||
// However, it is NOT safe for multiple processes on different machines
|
||||
// to share a cache directory (for example, if the directory were stored
|
||||
// in a network file system). File locking is notoriously unreliable in
|
||||
// network file systems and may not suffice to protect the cache.
|
||||
func Open(dir string) (*DiskCache, error) {
|
||||
info, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return nil, &os.PathError{Op: "open", Path: dir, Err: fmt.Errorf("not a directory")}
|
||||
}
|
||||
for i := range 256 {
|
||||
name := filepath.Join(dir, fmt.Sprintf("%02x", i))
|
||||
if err := os.MkdirAll(name, 0777); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c := &DiskCache{
|
||||
dir: dir,
|
||||
now: time.Now,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// fileName returns the name of the file corresponding to the given id.
|
||||
func (c *DiskCache) fileName(id [HashSize]byte, key string) string {
|
||||
return filepath.Join(c.dir, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+"-"+key)
|
||||
}
|
||||
|
||||
// An entryNotFoundError indicates that a cache entry was not found, with an
|
||||
// optional underlying reason.
|
||||
type entryNotFoundError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *entryNotFoundError) Error() string {
|
||||
if e.Err == nil {
|
||||
return "cache entry not found"
|
||||
}
|
||||
return fmt.Sprintf("cache entry not found: %v", e.Err)
|
||||
}
|
||||
|
||||
func (e *entryNotFoundError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
const (
|
||||
// action entry file is "v1 <hex id> <hex out> <decimal size space-padded to 20 bytes> <unixnano space-padded to 20 bytes>\n"
|
||||
hexSize = HashSize * 2
|
||||
entrySize = 2 + 1 + hexSize + 1 + hexSize + 1 + 20 + 1 + 20 + 1
|
||||
)
|
||||
|
||||
// verify controls whether to run the cache in verify mode.
|
||||
// In verify mode, the cache always returns errMissing from Get
|
||||
// but then double-checks in Put that the data being written
|
||||
// exactly matches any existing entry. This provides an easy
|
||||
// way to detect program behavior that would have been different
|
||||
// had the cache entry been returned from Get.
|
||||
//
|
||||
// verify is enabled by setting the environment variable
|
||||
// GODEBUG=gocacheverify=1.
|
||||
var verify = false
|
||||
|
||||
var errVerifyMode = errors.New("gocacheverify=1")
|
||||
|
||||
// DebugTest is set when GODEBUG=gocachetest=1 is in the environment.
|
||||
var DebugTest = false
|
||||
|
||||
func init() { initEnv() }
|
||||
|
||||
func initEnv() {
|
||||
verify = false
|
||||
debugHash = false
|
||||
debug := strings.SplitSeq(os.Getenv("GODEBUG"), ",")
|
||||
for f := range debug {
|
||||
if f == "gocacheverify=1" {
|
||||
verify = true
|
||||
}
|
||||
if f == "gocachehash=1" {
|
||||
debugHash = true
|
||||
}
|
||||
if f == "gocachetest=1" {
|
||||
DebugTest = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get looks up the action ID in the cache,
|
||||
// returning the corresponding output ID and file size, if any.
|
||||
// Note that finding an output ID does not guarantee that the
|
||||
// saved file for that output ID is still available.
|
||||
func (c *DiskCache) Get(id ActionID) (Entry, error) {
|
||||
if verify {
|
||||
return Entry{}, &entryNotFoundError{Err: errVerifyMode}
|
||||
}
|
||||
return c.get(id)
|
||||
}
|
||||
|
||||
type Entry struct {
|
||||
OutputID OutputID
|
||||
Size int64
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
// get is Get but does not respect verify mode, so that Put can use it.
|
||||
func (c *DiskCache) get(id ActionID) (Entry, error) {
|
||||
missing := func(reason error) (Entry, error) {
|
||||
return Entry{}, &entryNotFoundError{Err: reason}
|
||||
}
|
||||
f, err := os.Open(c.fileName(id, "a"))
|
||||
if err != nil {
|
||||
return missing(err)
|
||||
}
|
||||
defer f.Close()
|
||||
entry := make([]byte, entrySize+1) // +1 to detect whether f is too long
|
||||
if n, err := io.ReadFull(f, entry); n > entrySize {
|
||||
return missing(errors.New("too long"))
|
||||
} else if err != io.ErrUnexpectedEOF {
|
||||
if err == io.EOF {
|
||||
return missing(errors.New("file is empty"))
|
||||
}
|
||||
return missing(err)
|
||||
} else if n < entrySize {
|
||||
return missing(errors.New("entry file incomplete"))
|
||||
}
|
||||
if entry[0] != 'v' || entry[1] != '1' || entry[2] != ' ' || entry[3+hexSize] != ' ' || entry[3+hexSize+1+hexSize] != ' ' || entry[3+hexSize+1+hexSize+1+20] != ' ' || entry[entrySize-1] != '\n' {
|
||||
return missing(errors.New("invalid header"))
|
||||
}
|
||||
eid, entry := entry[3:3+hexSize], entry[3+hexSize:]
|
||||
eout, entry := entry[1:1+hexSize], entry[1+hexSize:]
|
||||
esize, entry := entry[1:1+20], entry[1+20:]
|
||||
//lint:ignore SA4006 See https://github.com/dominikh/go-tools/issues/465
|
||||
etime, entry := entry[1:1+20], entry[1+20:]
|
||||
var buf [HashSize]byte
|
||||
if _, err := hex.Decode(buf[:], eid); err != nil {
|
||||
return missing(fmt.Errorf("decoding ID: %v", err))
|
||||
} else if buf != id {
|
||||
return missing(errors.New("mismatched ID"))
|
||||
}
|
||||
if _, err := hex.Decode(buf[:], eout); err != nil {
|
||||
return missing(fmt.Errorf("decoding output ID: %v", err))
|
||||
}
|
||||
i := 0
|
||||
for i < len(esize) && esize[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
size, err := strconv.ParseInt(string(esize[i:]), 10, 64)
|
||||
if err != nil {
|
||||
return missing(fmt.Errorf("parsing size: %v", err))
|
||||
} else if size < 0 {
|
||||
return missing(errors.New("negative size"))
|
||||
}
|
||||
i = 0
|
||||
for i < len(etime) && etime[i] == ' ' {
|
||||
i++
|
||||
}
|
||||
tm, err := strconv.ParseInt(string(etime[i:]), 10, 64)
|
||||
if err != nil {
|
||||
return missing(fmt.Errorf("parsing timestamp: %v", err))
|
||||
} else if tm < 0 {
|
||||
return missing(errors.New("negative timestamp"))
|
||||
}
|
||||
|
||||
c.used(c.fileName(id, "a"))
|
||||
|
||||
return Entry{buf, size, time.Unix(0, tm)}, nil
|
||||
}
|
||||
|
||||
// GetFile looks up the action ID in the cache and returns
|
||||
// the name of the corresponding data file.
|
||||
func GetFile(c Cache, id ActionID) (file string, entry Entry, err error) {
|
||||
entry, err = c.Get(id)
|
||||
if err != nil {
|
||||
return "", Entry{}, err
|
||||
}
|
||||
file = c.OutputFile(entry.OutputID)
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return "", Entry{}, &entryNotFoundError{Err: err}
|
||||
}
|
||||
if info.Size() != entry.Size {
|
||||
return "", Entry{}, &entryNotFoundError{Err: errors.New("file incomplete")}
|
||||
}
|
||||
return file, entry, nil
|
||||
}
|
||||
|
||||
// GetBytes looks up the action ID in the cache and returns
|
||||
// the corresponding output bytes.
|
||||
// GetBytes should only be used for data that can be expected to fit in memory.
|
||||
func GetBytes(c Cache, id ActionID) ([]byte, Entry, error) {
|
||||
entry, err := c.Get(id)
|
||||
if err != nil {
|
||||
return nil, entry, err
|
||||
}
|
||||
data, _ := os.ReadFile(c.OutputFile(entry.OutputID))
|
||||
if sha256.Sum256(data) != entry.OutputID {
|
||||
return nil, entry, &entryNotFoundError{Err: errors.New("bad checksum")}
|
||||
}
|
||||
return data, entry, nil
|
||||
}
|
||||
|
||||
// OutputFile returns the name of the cache file storing output with the given OutputID.
|
||||
func (c *DiskCache) OutputFile(out OutputID) string {
|
||||
file := c.fileName(out, "d")
|
||||
c.used(file)
|
||||
return file
|
||||
}
|
||||
|
||||
// Time constants for cache expiration.
|
||||
//
|
||||
// We set the mtime on a cache file on each use, but at most one per mtimeInterval (1 hour),
|
||||
// to avoid causing many unnecessary inode updates. The mtimes therefore
|
||||
// roughly reflect "time of last use" but may in fact be older by at most an hour.
|
||||
//
|
||||
// We scan the cache for entries to delete at most once per trimInterval (1 day).
|
||||
//
|
||||
// When we do scan the cache, we delete entries that have not been used for
|
||||
// at least trimLimit (5 days). Statistics gathered from a month of usage by
|
||||
// Go developers found that essentially all reuse of cached entries happened
|
||||
// within 5 days of the previous reuse. See golang.org/issue/22990.
|
||||
const (
|
||||
mtimeInterval = 1 * time.Hour
|
||||
trimInterval = 24 * time.Hour
|
||||
trimLimit = 5 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// used makes a best-effort attempt to update mtime on file,
|
||||
// so that mtime reflects cache access time.
|
||||
//
|
||||
// Because the reflection only needs to be approximate,
|
||||
// and to reduce the amount of disk activity caused by using
|
||||
// cache entries, used only updates the mtime if the current
|
||||
// mtime is more than an hour old. This heuristic eliminates
|
||||
// nearly all of the mtime updates that would otherwise happen,
|
||||
// while still keeping the mtimes useful for cache trimming.
|
||||
func (c *DiskCache) used(file string) {
|
||||
info, err := os.Stat(file)
|
||||
if err == nil && c.now().Sub(info.ModTime()) < mtimeInterval {
|
||||
return
|
||||
}
|
||||
os.Chtimes(file, c.now(), c.now())
|
||||
}
|
||||
|
||||
// Trim removes old cache entries that are likely not to be reused.
|
||||
func (c *DiskCache) Trim() {
|
||||
now := c.now()
|
||||
|
||||
// We maintain in dir/trim.txt the time of the last completed cache trim.
|
||||
// If the cache has been trimmed recently enough, do nothing.
|
||||
// This is the common case.
|
||||
data, _ := renameio.ReadFile(filepath.Join(c.dir, "trim.txt"))
|
||||
t, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64)
|
||||
if err == nil && now.Sub(time.Unix(t, 0)) < trimInterval {
|
||||
return
|
||||
}
|
||||
|
||||
// Trim each of the 256 subdirectories.
|
||||
// We subtract an additional mtimeInterval
|
||||
// to account for the imprecision of our "last used" mtimes.
|
||||
cutoff := now.Add(-trimLimit - mtimeInterval)
|
||||
for i := range 256 {
|
||||
subdir := filepath.Join(c.dir, fmt.Sprintf("%02x", i))
|
||||
c.trimSubdir(subdir, cutoff)
|
||||
}
|
||||
|
||||
// Ignore errors from here: if we don't write the complete timestamp, the
|
||||
// cache will appear older than it is, and we'll trim it again next time.
|
||||
renameio.WriteFile(filepath.Join(c.dir, "trim.txt"), fmt.Appendf(nil, "%d", now.Unix()), 0666)
|
||||
}
|
||||
|
||||
// Close trims the cache and releases resources.
|
||||
func (c *DiskCache) Close() error {
|
||||
c.Trim()
|
||||
return nil
|
||||
}
|
||||
|
||||
// trimSubdir trims a single cache subdirectory.
|
||||
func (c *DiskCache) trimSubdir(subdir string, cutoff time.Time) {
|
||||
// Read all directory entries from subdir before removing
|
||||
// any files, in case removing files invalidates the file offset
|
||||
// in the directory scan. Also, ignore error from f.Readdirnames,
|
||||
// because we don't care about reporting the error and we still
|
||||
// want to process any entries found before the error.
|
||||
f, err := os.Open(subdir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
names, _ := f.Readdirnames(-1)
|
||||
f.Close()
|
||||
|
||||
for _, name := range names {
|
||||
// Remove only cache entries (xxxx-a and xxxx-d).
|
||||
if !strings.HasSuffix(name, "-a") && !strings.HasSuffix(name, "-d") {
|
||||
continue
|
||||
}
|
||||
entry := filepath.Join(subdir, name)
|
||||
info, err := os.Stat(entry)
|
||||
if err == nil && info.ModTime().Before(cutoff) {
|
||||
os.Remove(entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// putIndexEntry adds an entry to the cache recording that executing the action
|
||||
// with the given id produces an output with the given output id (hash) and size.
|
||||
func (c *DiskCache) putIndexEntry(id ActionID, out OutputID, size int64, allowVerify bool) error {
|
||||
// Note: We expect that for one reason or another it may happen
|
||||
// that repeating an action produces a different output hash
|
||||
// (for example, if the output contains a time stamp or temp dir name).
|
||||
// While not ideal, this is also not a correctness problem, so we
|
||||
// don't make a big deal about it. In particular, we leave the action
|
||||
// cache entries writable specifically so that they can be overwritten.
|
||||
//
|
||||
// Setting GODEBUG=gocacheverify=1 does make a big deal:
|
||||
// in verify mode we are double-checking that the cache entries
|
||||
// are entirely reproducible. As just noted, this may be unrealistic
|
||||
// in some cases but the check is also useful for shaking out real bugs.
|
||||
entry := fmt.Sprintf("v1 %x %x %20d %20d\n", id, out, size, time.Now().UnixNano())
|
||||
if verify && allowVerify {
|
||||
old, err := c.get(id)
|
||||
if err == nil && (old.OutputID != out || old.Size != size) {
|
||||
// panic to show stack trace, so we can see what code is generating this cache entry.
|
||||
msg := fmt.Sprintf("go: internal cache error: cache verify failed: id=%x changed:<<<\n%s\n>>>\nold: %x %d\nnew: %x %d", id, reverseHash(id), out, size, old.OutputID, old.Size)
|
||||
panic(msg)
|
||||
}
|
||||
}
|
||||
file := c.fileName(id, "a")
|
||||
|
||||
// Copy file to cache directory.
|
||||
mode := os.O_WRONLY | os.O_CREATE
|
||||
f, err := os.OpenFile(file, mode, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = f.WriteString(entry)
|
||||
if err == nil {
|
||||
// Truncate the file only *after* writing it.
|
||||
// (This should be a no-op, but truncate just in case of previous corruption.)
|
||||
//
|
||||
// This differs from ioutil.WriteFile, which truncates to 0 *before* writing
|
||||
// via os.O_TRUNC. Truncating only after writing ensures that a second write
|
||||
// of the same content to the same file is idempotent, and does not — even
|
||||
// temporarily! — undo the effect of the first write.
|
||||
err = f.Truncate(int64(len(entry)))
|
||||
}
|
||||
if closeErr := f.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
// TODO(bcmills): This Remove potentially races with another go command writing to file.
|
||||
// Can we eliminate it?
|
||||
os.Remove(file)
|
||||
return err
|
||||
}
|
||||
os.Chtimes(file, c.now(), c.now()) // mainly for tests
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Put stores the given output in the cache as the output for the action ID.
|
||||
// It may read file twice. The content of file must not change between the two passes.
|
||||
func (c *DiskCache) Put(id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
|
||||
// Unwrap noVerifyReadSeeker to determine verify mode.
|
||||
allowVerify := true
|
||||
if nv, ok := file.(noVerifyReadSeeker); ok {
|
||||
allowVerify = false
|
||||
file = nv.ReadSeeker
|
||||
}
|
||||
return c.put(id, file, allowVerify)
|
||||
}
|
||||
|
||||
// noVerifyReadSeeker is an io.ReadSeeker wrapper that indicates that
|
||||
// the Put should skip GODEBUG=gocacheverify=1 verification.
|
||||
type noVerifyReadSeeker struct {
|
||||
io.ReadSeeker
|
||||
}
|
||||
|
||||
// PutNoVerify is like Cache.Put but disables the verify check
|
||||
// when GODEBUG=gocacheverify=1 is set.
|
||||
// It is meant for data that is OK to cache but that we expect to vary slightly
|
||||
// from run to run, like test output containing times and the like.
|
||||
func PutNoVerify(c Cache, id ActionID, file io.ReadSeeker) (OutputID, int64, error) {
|
||||
return c.Put(id, noVerifyReadSeeker{file})
|
||||
}
|
||||
|
||||
func (c *DiskCache) put(id ActionID, file io.ReadSeeker, allowVerify bool) (OutputID, int64, error) {
|
||||
// Compute output ID.
|
||||
h := sha256.New()
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return OutputID{}, 0, err
|
||||
}
|
||||
size, err := io.Copy(h, file)
|
||||
if err != nil {
|
||||
return OutputID{}, 0, err
|
||||
}
|
||||
var out OutputID
|
||||
h.Sum(out[:0])
|
||||
|
||||
// Copy to cached output file (if not already present).
|
||||
if err := c.copyFile(file, out, size); err != nil {
|
||||
return out, size, err
|
||||
}
|
||||
|
||||
// Add to cache index.
|
||||
return out, size, c.putIndexEntry(id, out, size, allowVerify)
|
||||
}
|
||||
|
||||
// PutBytes stores the given bytes in the cache as the output for the action ID.
|
||||
func PutBytes(c Cache, id ActionID, data []byte) error {
|
||||
_, _, err := c.Put(id, bytes.NewReader(data))
|
||||
return err
|
||||
}
|
||||
|
||||
// copyFile copies file into the cache, expecting it to have the given
|
||||
// output ID and size, if that file is not present already.
|
||||
func (c *DiskCache) copyFile(file io.ReadSeeker, out OutputID, size int64) error {
|
||||
name := c.fileName(out, "d")
|
||||
info, err := os.Stat(name)
|
||||
if err == nil && info.Size() == size {
|
||||
// Check hash.
|
||||
if f, err := os.Open(name); err == nil {
|
||||
h := sha256.New()
|
||||
io.Copy(h, f)
|
||||
f.Close()
|
||||
var out2 OutputID
|
||||
h.Sum(out2[:0])
|
||||
if out == out2 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Hash did not match. Fall through and rewrite file.
|
||||
}
|
||||
|
||||
// Copy file to cache directory.
|
||||
mode := os.O_RDWR | os.O_CREATE
|
||||
if err == nil && info.Size() > size { // shouldn't happen but fix in case
|
||||
mode |= os.O_TRUNC
|
||||
}
|
||||
f, err := os.OpenFile(name, mode, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if size == 0 {
|
||||
// File now exists with correct size.
|
||||
// Only one possible zero-length file, so contents are OK too.
|
||||
// Early return here makes sure there's a "last byte" for code below.
|
||||
return nil
|
||||
}
|
||||
|
||||
// From here on, if any of the I/O writing the file fails,
|
||||
// we make a best-effort attempt to truncate the file f
|
||||
// before returning, to avoid leaving bad bytes in the file.
|
||||
|
||||
// Copy file to f, but also into h to double-check hash.
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
f.Truncate(0)
|
||||
return err
|
||||
}
|
||||
h := sha256.New()
|
||||
w := io.MultiWriter(f, h)
|
||||
if _, err := io.CopyN(w, file, size-1); err != nil {
|
||||
f.Truncate(0)
|
||||
return err
|
||||
}
|
||||
// Check last byte before writing it; writing it will make the size match
|
||||
// what other processes expect to find and might cause them to start
|
||||
// using the file.
|
||||
buf := make([]byte, 1)
|
||||
if _, err := file.Read(buf); err != nil {
|
||||
f.Truncate(0)
|
||||
return err
|
||||
}
|
||||
h.Write(buf)
|
||||
sum := h.Sum(nil)
|
||||
if !bytes.Equal(sum, out[:]) {
|
||||
f.Truncate(0)
|
||||
return fmt.Errorf("file content changed underfoot")
|
||||
}
|
||||
|
||||
// Commit cache file entry.
|
||||
if _, err := f.Write(buf); err != nil {
|
||||
f.Truncate(0)
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
// Data might not have been written,
|
||||
// but file may look like it is the right size.
|
||||
// To be extra careful, remove cached file.
|
||||
os.Remove(name)
|
||||
return err
|
||||
}
|
||||
os.Chtimes(name, c.now(), c.now()) // mainly for tests
|
||||
|
||||
return nil
|
||||
}
|
||||
111
vendor/honnef.co/go/tools/lintcmd/cache/cacheprog/cacheprog.go
vendored
Normal file
111
vendor/honnef.co/go/tools/lintcmd/cache/cacheprog/cacheprog.go
vendored
Normal file
@@ -0,0 +1,111 @@
|
||||
// Copyright 2024 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 cacheprog defines the protocol for a GOCACHEPROG program.
|
||||
//
|
||||
// This is a port of cmd/go/internal/cacheprog from the Go standard library,
|
||||
// adapted for use with staticcheck.
|
||||
package cacheprog
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Cmd is a command that can be issued to a child process.
|
||||
//
|
||||
// If the interface needs to grow, new commands or new versioned commands like
|
||||
// "get2" can be added in the future. The initial [Response] from the child
|
||||
// process indicates which commands it supports.
|
||||
type Cmd string
|
||||
|
||||
const (
|
||||
// CmdPut tells the cache program to store an object in the cache.
|
||||
//
|
||||
// [Request.ActionID] is the cache key of this object. The cache should
|
||||
// store [Request.OutputID] and [Request.Body] under this key for a
|
||||
// later "get" request. It must also store the Body in a file in the local
|
||||
// file system and return the path to that file in [Response.DiskPath],
|
||||
// which must exist at least until a "close" request.
|
||||
CmdPut = Cmd("put")
|
||||
|
||||
// CmdGet tells the cache program to retrieve an object from the cache.
|
||||
//
|
||||
// [Request.ActionID] specifies the key of the object to get. If the
|
||||
// cache does not contain this object, it should set [Response.Miss] to
|
||||
// true. Otherwise, it should populate the fields of [Response],
|
||||
// including setting [Response.OutputID] to the OutputID of the original
|
||||
// "put" request and [Response.DiskPath] to the path of a local file
|
||||
// containing the Body of the original "put" request. That file must
|
||||
// continue to exist at least until a "close" request.
|
||||
CmdGet = Cmd("get")
|
||||
|
||||
// CmdClose requests that the cache program exit gracefully.
|
||||
//
|
||||
// The cache program should reply to this request and then exit
|
||||
// (thus closing its stdout).
|
||||
CmdClose = Cmd("close")
|
||||
)
|
||||
|
||||
// Request is the JSON-encoded message that's sent from staticcheck to
|
||||
// the GOCACHEPROG child process over stdin. Each JSON object is on
|
||||
// its own line. A Request of type "put" with BodySize > 0 will be followed by
|
||||
// a line containing a base64-encoded JSON string literal of the body.
|
||||
type Request struct {
|
||||
// ID is a unique number per process across all requests.
|
||||
// It must be echoed in the Response from the child.
|
||||
ID int64
|
||||
|
||||
// Command is the type of request.
|
||||
// Only commands that were declared as supported by the child will be sent.
|
||||
Command Cmd
|
||||
|
||||
// ActionID is the cache key for "put" and "get" requests.
|
||||
ActionID []byte `json:",omitempty"` // or nil if not used
|
||||
|
||||
// OutputID is stored with the body for "put" requests.
|
||||
OutputID []byte `json:",omitempty"` // or nil if not used
|
||||
|
||||
// Body is the body for "put" requests. It's sent after the JSON object
|
||||
// as a base64-encoded JSON string when BodySize is non-zero.
|
||||
Body io.Reader `json:"-"`
|
||||
|
||||
// BodySize is the number of bytes of Body. If zero, the body isn't written.
|
||||
BodySize int64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
// Response is the JSON response from the child process to staticcheck.
|
||||
//
|
||||
// With the exception of the first protocol message that the child writes to its
|
||||
// stdout with ID==0 and KnownCommands populated, these are only sent in
|
||||
// response to a Request from staticcheck.
|
||||
//
|
||||
// Responses can be sent in any order. The ID must match the request they're
|
||||
// replying to.
|
||||
type Response struct {
|
||||
ID int64 // that corresponds to Request; they can be answered out of order
|
||||
Err string `json:",omitempty"` // if non-empty, the error
|
||||
|
||||
// KnownCommands is included in the first message that cache helper program
|
||||
// writes to stdout on startup (with ID==0). It includes the
|
||||
// Request.Command types that are supported by the program.
|
||||
//
|
||||
// This lets the protocol be extended gracefully over time (adding
|
||||
// "get2", etc), or fail gracefully when needed. It also lets us
|
||||
// verify the program wants to be a cache helper.
|
||||
KnownCommands []Cmd `json:",omitempty"`
|
||||
|
||||
// For "get" requests.
|
||||
|
||||
Miss bool `json:",omitempty"` // cache miss
|
||||
OutputID []byte `json:",omitempty"` // the OutputID stored with the body
|
||||
Size int64 `json:",omitempty"` // body size in bytes
|
||||
Time *time.Time `json:",omitempty"` // when the object was put in the cache (optional; used for cache expiration)
|
||||
|
||||
// For "get" and "put" requests.
|
||||
|
||||
// DiskPath is the absolute path on disk of the body corresponding to a
|
||||
// "get" (on cache hit) or "put" request's ActionID.
|
||||
DiskPath string `json:",omitempty"`
|
||||
}
|
||||
89
vendor/honnef.co/go/tools/lintcmd/cache/default.go
vendored
Normal file
89
vendor/honnef.co/go/tools/lintcmd/cache/default.go
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright 2017 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 cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Default returns the default cache to use.
|
||||
func Default() (Cache, error) {
|
||||
defaultOnce.Do(initDefaultCache)
|
||||
return defaultCache, defaultDirErr
|
||||
}
|
||||
|
||||
var (
|
||||
defaultOnce sync.Once
|
||||
defaultCache Cache
|
||||
)
|
||||
|
||||
// cacheREADME is a message stored in a README in the cache directory.
|
||||
// Because the cache lives outside the normal Go trees, we leave the
|
||||
// README as a courtesy to explain where it came from.
|
||||
const cacheREADME = `This directory holds cached build artifacts from staticcheck.
|
||||
`
|
||||
|
||||
// initDefaultCache does the work of finding the default cache
|
||||
// the first time Default is called.
|
||||
func initDefaultCache() {
|
||||
dir := DefaultDir()
|
||||
if err := os.MkdirAll(dir, 0777); err != nil {
|
||||
log.Fatalf("failed to initialize build cache at %s: %s\n", dir, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "README")); err != nil {
|
||||
// Best effort.
|
||||
os.WriteFile(filepath.Join(dir, "README"), []byte(cacheREADME), 0666)
|
||||
}
|
||||
|
||||
c, err := Open(dir)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to initialize build cache at %s: %s\n", dir, err)
|
||||
}
|
||||
|
||||
if prog := os.Getenv("GOCACHEPROG"); prog != "" {
|
||||
defaultCache = startCacheProg(prog)
|
||||
} else {
|
||||
defaultCache = c
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
defaultDirOnce sync.Once
|
||||
defaultDir string
|
||||
defaultDirErr error
|
||||
)
|
||||
|
||||
// DefaultDir returns the effective STATICCHECK_CACHE setting.
|
||||
func DefaultDir() string {
|
||||
// Save the result of the first call to DefaultDir for later use in
|
||||
// initDefaultCache. cmd/go/main.go explicitly sets GOCACHE so that
|
||||
// subprocesses will inherit it, but that means initDefaultCache can't
|
||||
// otherwise distinguish between an explicit "off" and a UserCacheDir error.
|
||||
|
||||
defaultDirOnce.Do(func() {
|
||||
defaultDir = os.Getenv("STATICCHECK_CACHE")
|
||||
if filepath.IsAbs(defaultDir) {
|
||||
return
|
||||
}
|
||||
if defaultDir != "" {
|
||||
defaultDirErr = fmt.Errorf("STATICCHECK_CACHE is not an absolute path")
|
||||
return
|
||||
}
|
||||
|
||||
// Compute default location.
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
defaultDirErr = fmt.Errorf("STATICCHECK_CACHE is not defined and %v", err)
|
||||
return
|
||||
}
|
||||
defaultDir = filepath.Join(dir, "staticcheck")
|
||||
})
|
||||
|
||||
return defaultDir
|
||||
}
|
||||
176
vendor/honnef.co/go/tools/lintcmd/cache/hash.go
vendored
Normal file
176
vendor/honnef.co/go/tools/lintcmd/cache/hash.go
vendored
Normal file
@@ -0,0 +1,176 @@
|
||||
// Copyright 2017 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 cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var debugHash = false // set when GODEBUG=gocachehash=1
|
||||
|
||||
// HashSize is the number of bytes in a hash.
|
||||
const HashSize = 32
|
||||
|
||||
// hashSalt is an optional salt prepended to every hash created by NewHash.
|
||||
// When set (via SetSalt), it is typically the build ID of the current binary,
|
||||
// so that cache entries are invalidated when the binary changes.
|
||||
var hashSalt []byte
|
||||
|
||||
// A Hash provides access to the canonical hash function used to index the cache.
|
||||
// The current implementation uses salted SHA256, but clients must not assume this.
|
||||
type Hash struct {
|
||||
h hash.Hash
|
||||
name string // for debugging
|
||||
buf *bytes.Buffer // for verify
|
||||
}
|
||||
|
||||
// Subkey returns an action ID corresponding to mixing a parent
|
||||
// action ID with a string description of the subkey.
|
||||
func Subkey(parent ActionID, desc string) ActionID {
|
||||
h := sha256.New()
|
||||
h.Write([]byte("subkey:"))
|
||||
h.Write(parent[:])
|
||||
h.Write([]byte(desc))
|
||||
var out ActionID
|
||||
h.Sum(out[:0])
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH subkey %x %q = %x\n", parent, desc, out)
|
||||
}
|
||||
if verify {
|
||||
hashDebug.Lock()
|
||||
hashDebug.m[out] = fmt.Sprintf("subkey %x %q", parent, desc)
|
||||
hashDebug.Unlock()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SetSalt sets an optional salt that is prepended to every hash created by
|
||||
// NewHash. This is typically the build ID of the current binary, ensuring that
|
||||
// cache entries are invalidated when the binary changes. If not called,
|
||||
// NewHash operates without a salt.
|
||||
func SetSalt(b []byte) {
|
||||
hashSalt = b
|
||||
}
|
||||
|
||||
// NewHash returns a new Hash.
|
||||
// The caller is expected to Write data to it and then call Sum.
|
||||
func NewHash(name string) *Hash {
|
||||
h := &Hash{h: sha256.New(), name: name}
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH[%s]\n", h.name)
|
||||
}
|
||||
h.Write(hashSalt)
|
||||
if verify {
|
||||
h.buf = new(bytes.Buffer)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Write writes data to the running hash.
|
||||
func (h *Hash) Write(b []byte) (int, error) {
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH[%s]: %q\n", h.name, b)
|
||||
}
|
||||
if h.buf != nil {
|
||||
h.buf.Write(b)
|
||||
}
|
||||
return h.h.Write(b)
|
||||
}
|
||||
|
||||
// Sum returns the hash of the data written previously.
|
||||
func (h *Hash) Sum() [HashSize]byte {
|
||||
var out [HashSize]byte
|
||||
h.h.Sum(out[:0])
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH[%s]: %x\n", h.name, out)
|
||||
}
|
||||
if h.buf != nil {
|
||||
hashDebug.Lock()
|
||||
if hashDebug.m == nil {
|
||||
hashDebug.m = make(map[[HashSize]byte]string)
|
||||
}
|
||||
hashDebug.m[out] = h.buf.String()
|
||||
hashDebug.Unlock()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// In GODEBUG=gocacheverify=1 mode,
|
||||
// hashDebug holds the input to every computed hash ID,
|
||||
// so that we can work backward from the ID involved in a
|
||||
// cache entry mismatch to a description of what should be there.
|
||||
var hashDebug struct {
|
||||
sync.Mutex
|
||||
m map[[HashSize]byte]string
|
||||
}
|
||||
|
||||
// reverseHash returns the input used to compute the hash id.
|
||||
func reverseHash(id [HashSize]byte) string {
|
||||
hashDebug.Lock()
|
||||
s := hashDebug.m[id]
|
||||
hashDebug.Unlock()
|
||||
return s
|
||||
}
|
||||
|
||||
var hashFileCache struct {
|
||||
sync.Mutex
|
||||
m map[string][HashSize]byte
|
||||
}
|
||||
|
||||
// FileHash returns the hash of the named file.
|
||||
// It caches repeated lookups for a given file,
|
||||
// and the cache entry for a file can be initialized
|
||||
// using SetFileHash.
|
||||
// The hash used by FileHash is not the same as
|
||||
// the hash used by NewHash.
|
||||
func FileHash(file string) ([HashSize]byte, error) {
|
||||
hashFileCache.Lock()
|
||||
out, ok := hashFileCache.m[file]
|
||||
hashFileCache.Unlock()
|
||||
|
||||
if ok {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH %s: %v\n", file, err)
|
||||
}
|
||||
return [HashSize]byte{}, err
|
||||
}
|
||||
_, err = io.Copy(h, f)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH %s: %v\n", file, err)
|
||||
}
|
||||
return [HashSize]byte{}, err
|
||||
}
|
||||
h.Sum(out[:0])
|
||||
if debugHash {
|
||||
fmt.Fprintf(os.Stderr, "HASH %s: %x\n", file, out)
|
||||
}
|
||||
|
||||
SetFileHash(file, out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SetFileHash sets the hash returned by FileHash for file.
|
||||
func SetFileHash(file string, sum [HashSize]byte) {
|
||||
hashFileCache.Lock()
|
||||
if hashFileCache.m == nil {
|
||||
hashFileCache.m = make(map[string][HashSize]byte)
|
||||
}
|
||||
hashFileCache.m[file] = sum
|
||||
hashFileCache.Unlock()
|
||||
}
|
||||
353
vendor/honnef.co/go/tools/lintcmd/cache/prog.go
vendored
Normal file
353
vendor/honnef.co/go/tools/lintcmd/cache/prog.go
vendored
Normal file
@@ -0,0 +1,353 @@
|
||||
// Copyright 2023 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 cache
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"honnef.co/go/tools/lintcmd/cache/cacheprog"
|
||||
)
|
||||
|
||||
// ProgCache implements Cache via JSON messages over stdin/stdout to a child
|
||||
// helper process which can then implement whatever caching policy/mechanism it
|
||||
// wants.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/59719
|
||||
type ProgCache struct {
|
||||
cmd *exec.Cmd
|
||||
stdout io.ReadCloser // from the child process
|
||||
stdin io.WriteCloser // to the child process
|
||||
bw *bufio.Writer // to stdin
|
||||
jenc *json.Encoder // to bw
|
||||
|
||||
// can are the commands that the child process declared that it supports.
|
||||
// This is effectively the versioning mechanism.
|
||||
can map[cacheprog.Cmd]bool
|
||||
|
||||
closing atomic.Bool
|
||||
ctx context.Context // valid until Close via ctxCancel
|
||||
ctxCancel context.CancelFunc // called on Close
|
||||
readLoopDone chan struct{} // closed when readLoop returns
|
||||
|
||||
mu sync.Mutex // guards following fields
|
||||
nextID int64
|
||||
inFlight map[int64]chan<- *cacheprog.Response
|
||||
outputFile map[OutputID]string // object => abs path on disk
|
||||
|
||||
// writeMu serializes writing to the child process.
|
||||
// It must never be held at the same time as mu.
|
||||
writeMu sync.Mutex
|
||||
}
|
||||
|
||||
// startCacheProg starts the prog binary (with optional space-separated flags)
|
||||
// and returns a Cache implementation that talks to it.
|
||||
//
|
||||
// It blocks a few seconds to wait for the child process to successfully start
|
||||
// and advertise its capabilities.
|
||||
func startCacheProg(progAndArgs string) Cache {
|
||||
args := strings.Fields(progAndArgs)
|
||||
var prog string
|
||||
if len(args) > 0 {
|
||||
prog = args[0]
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
ctx, ctxCancel := context.WithCancel(context.Background())
|
||||
|
||||
cmd := exec.CommandContext(ctx, prog, args...)
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("StdoutPipe to GOCACHEPROG: %v", err)
|
||||
}
|
||||
in, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Fatalf("StdinPipe to GOCACHEPROG: %v", err)
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
// On close, we cancel the context. Rather than killing the helper,
|
||||
// close its stdin.
|
||||
cmd.Cancel = in.Close
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Fatalf("error starting GOCACHEPROG program %q: %v", prog, err)
|
||||
}
|
||||
|
||||
pc := &ProgCache{
|
||||
ctx: ctx,
|
||||
ctxCancel: ctxCancel,
|
||||
cmd: cmd,
|
||||
stdout: out,
|
||||
stdin: in,
|
||||
bw: bufio.NewWriter(in),
|
||||
inFlight: make(map[int64]chan<- *cacheprog.Response),
|
||||
outputFile: make(map[OutputID]string),
|
||||
readLoopDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Register our interest in the initial protocol message from the child to
|
||||
// us, saying what it can do.
|
||||
capResc := make(chan *cacheprog.Response, 1)
|
||||
pc.inFlight[0] = capResc
|
||||
|
||||
pc.jenc = json.NewEncoder(pc.bw)
|
||||
go pc.readLoop(pc.readLoopDone)
|
||||
|
||||
// Give the child process a few seconds to report its capabilities. This
|
||||
// should be instant and not require any slow work by the program.
|
||||
timer := time.NewTicker(5 * time.Second)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
log.Printf("# still waiting for GOCACHEPROG %v ...", prog)
|
||||
case capRes := <-capResc:
|
||||
can := map[cacheprog.Cmd]bool{}
|
||||
for _, cmd := range capRes.KnownCommands {
|
||||
can[cmd] = true
|
||||
}
|
||||
if len(can) == 0 {
|
||||
log.Fatalf("GOCACHEPROG %v declared no supported commands", prog)
|
||||
}
|
||||
pc.can = can
|
||||
return pc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProgCache) readLoop(readLoopDone chan<- struct{}) {
|
||||
defer close(readLoopDone)
|
||||
jd := json.NewDecoder(c.stdout)
|
||||
for {
|
||||
res := new(cacheprog.Response)
|
||||
if err := jd.Decode(res); err != nil {
|
||||
if c.closing.Load() {
|
||||
c.mu.Lock()
|
||||
for _, ch := range c.inFlight {
|
||||
close(ch)
|
||||
}
|
||||
c.inFlight = nil
|
||||
c.mu.Unlock()
|
||||
return // quietly
|
||||
}
|
||||
if err == io.EOF {
|
||||
c.mu.Lock()
|
||||
inFlight := len(c.inFlight)
|
||||
c.mu.Unlock()
|
||||
log.Fatalf("GOCACHEPROG exited pre-Close with %v pending requests", inFlight)
|
||||
}
|
||||
log.Fatalf("error reading JSON from GOCACHEPROG: %v", err)
|
||||
}
|
||||
c.mu.Lock()
|
||||
ch, ok := c.inFlight[res.ID]
|
||||
delete(c.inFlight, res.ID)
|
||||
c.mu.Unlock()
|
||||
if ok {
|
||||
ch <- res
|
||||
} else {
|
||||
log.Fatalf("GOCACHEPROG sent response for unknown request ID %v", res.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errCacheprogClosed = errors.New("GOCACHEPROG program closed unexpectedly")
|
||||
|
||||
func (c *ProgCache) send(ctx context.Context, req *cacheprog.Request) (*cacheprog.Response, error) {
|
||||
resc := make(chan *cacheprog.Response, 1)
|
||||
if err := c.writeToChild(req, resc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case res := <-resc:
|
||||
if res == nil {
|
||||
return nil, errCacheprogClosed
|
||||
}
|
||||
if res.Err != "" {
|
||||
return nil, errors.New(res.Err)
|
||||
}
|
||||
return res, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ProgCache) writeToChild(req *cacheprog.Request, resc chan<- *cacheprog.Response) (err error) {
|
||||
c.mu.Lock()
|
||||
if c.inFlight == nil {
|
||||
c.mu.Unlock()
|
||||
return errCacheprogClosed
|
||||
}
|
||||
c.nextID++
|
||||
req.ID = c.nextID
|
||||
c.inFlight[req.ID] = resc
|
||||
c.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.mu.Lock()
|
||||
if c.inFlight != nil {
|
||||
delete(c.inFlight, req.ID)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
c.writeMu.Lock()
|
||||
defer c.writeMu.Unlock()
|
||||
|
||||
if err := c.jenc.Encode(req); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := c.bw.WriteByte('\n'); err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Body != nil && req.BodySize > 0 {
|
||||
if err := c.bw.WriteByte('"'); err != nil {
|
||||
return err
|
||||
}
|
||||
e := base64.NewEncoder(base64.StdEncoding, c.bw)
|
||||
wrote, err := io.Copy(e, req.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := e.Close(); err != nil {
|
||||
return nil
|
||||
}
|
||||
if wrote != req.BodySize {
|
||||
return fmt.Errorf("short write writing body to GOCACHEPROG for action %x, output %x: wrote %v; expected %v",
|
||||
req.ActionID, req.OutputID, wrote, req.BodySize)
|
||||
}
|
||||
if _, err := c.bw.WriteString("\"\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := c.bw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ProgCache) Get(a ActionID) (Entry, error) {
|
||||
if !c.can[cacheprog.CmdGet] {
|
||||
// They can't do a "get". Maybe they're a write-only cache.
|
||||
return Entry{}, &entryNotFoundError{}
|
||||
}
|
||||
res, err := c.send(c.ctx, &cacheprog.Request{
|
||||
Command: cacheprog.CmdGet,
|
||||
ActionID: a[:],
|
||||
})
|
||||
if err != nil {
|
||||
return Entry{}, err
|
||||
}
|
||||
if res.Miss {
|
||||
return Entry{}, &entryNotFoundError{}
|
||||
}
|
||||
e := Entry{
|
||||
Size: res.Size,
|
||||
}
|
||||
if res.Time != nil {
|
||||
e.Time = *res.Time
|
||||
} else {
|
||||
e.Time = time.Now()
|
||||
}
|
||||
if res.DiskPath == "" {
|
||||
return Entry{}, &entryNotFoundError{errors.New("GOCACHEPROG didn't populate DiskPath on get hit")}
|
||||
}
|
||||
if copy(e.OutputID[:], res.OutputID) != len(res.OutputID) {
|
||||
return Entry{}, &entryNotFoundError{errors.New("incomplete ProgResponse OutputID")}
|
||||
}
|
||||
c.noteOutputFile(e.OutputID, res.DiskPath)
|
||||
return e, nil
|
||||
}
|
||||
|
||||
func (c *ProgCache) noteOutputFile(o OutputID, diskPath string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.outputFile[o] = diskPath
|
||||
}
|
||||
|
||||
func (c *ProgCache) OutputFile(o OutputID) string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.outputFile[o]
|
||||
}
|
||||
|
||||
func (c *ProgCache) Put(a ActionID, file io.ReadSeeker) (_ OutputID, size int64, _ error) {
|
||||
// Unwrap noVerifyReadSeeker; ProgCache doesn't use the verify mode.
|
||||
if nv, ok := file.(noVerifyReadSeeker); ok {
|
||||
file = nv.ReadSeeker
|
||||
}
|
||||
|
||||
// Compute output ID.
|
||||
h := sha256.New()
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return OutputID{}, 0, err
|
||||
}
|
||||
size, err := io.Copy(h, file)
|
||||
if err != nil {
|
||||
return OutputID{}, 0, err
|
||||
}
|
||||
var out OutputID
|
||||
h.Sum(out[:0])
|
||||
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return OutputID{}, 0, err
|
||||
}
|
||||
|
||||
if !c.can[cacheprog.CmdPut] {
|
||||
// Child is a read-only cache. Do nothing.
|
||||
return out, size, nil
|
||||
}
|
||||
|
||||
res, err := c.send(c.ctx, &cacheprog.Request{
|
||||
Command: cacheprog.CmdPut,
|
||||
ActionID: a[:],
|
||||
OutputID: out[:],
|
||||
Body: file,
|
||||
BodySize: size,
|
||||
})
|
||||
if err != nil {
|
||||
return OutputID{}, 0, err
|
||||
}
|
||||
if res.DiskPath == "" {
|
||||
return OutputID{}, 0, errors.New("GOCACHEPROG didn't return DiskPath in put response")
|
||||
}
|
||||
c.noteOutputFile(out, res.DiskPath)
|
||||
return out, size, err
|
||||
}
|
||||
|
||||
func (c *ProgCache) Close() error {
|
||||
c.closing.Store(true)
|
||||
var err error
|
||||
|
||||
// First write a "close" message to the child so it can exit nicely
|
||||
// and clean up if it wants. Only after that exchange do we cancel
|
||||
// the context that kills the process.
|
||||
if c.can[cacheprog.CmdClose] {
|
||||
_, err = c.send(c.ctx, &cacheprog.Request{Command: cacheprog.CmdClose})
|
||||
if errors.Is(err, errCacheprogClosed) {
|
||||
// Allow the child to quit without responding to close.
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
// Cancel the context, which will close the helper's stdin.
|
||||
c.ctxCancel()
|
||||
// Wait until the helper closes its stdout.
|
||||
<-c.readLoopDone
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user