src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

prog.go (8860B)


      1 // Copyright 2023 The Go Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style
      3 // license that can be found in the LICENSE file.
      4 
      5 package cache
      6 
      7 import (
      8 	"bufio"
      9 	"context"
     10 	"crypto/sha256"
     11 	"encoding/base64"
     12 	"encoding/json"
     13 	"errors"
     14 	"fmt"
     15 	"io"
     16 	"log"
     17 	"os"
     18 	"os/exec"
     19 	"strings"
     20 	"sync"
     21 	"sync/atomic"
     22 	"time"
     23 
     24 	"honnef.co/go/tools/lintcmd/cache/cacheprog"
     25 )
     26 
     27 // ProgCache implements Cache via JSON messages over stdin/stdout to a child
     28 // helper process which can then implement whatever caching policy/mechanism it
     29 // wants.
     30 //
     31 // See https://github.com/golang/go/issues/59719
     32 type ProgCache struct {
     33 	cmd    *exec.Cmd
     34 	stdout io.ReadCloser  // from the child process
     35 	stdin  io.WriteCloser // to the child process
     36 	bw     *bufio.Writer  // to stdin
     37 	jenc   *json.Encoder  // to bw
     38 
     39 	// can are the commands that the child process declared that it supports.
     40 	// This is effectively the versioning mechanism.
     41 	can map[cacheprog.Cmd]bool
     42 
     43 	closing      atomic.Bool
     44 	ctx          context.Context    // valid until Close via ctxCancel
     45 	ctxCancel    context.CancelFunc // called on Close
     46 	readLoopDone chan struct{}      // closed when readLoop returns
     47 
     48 	mu         sync.Mutex // guards following fields
     49 	nextID     int64
     50 	inFlight   map[int64]chan<- *cacheprog.Response
     51 	outputFile map[OutputID]string // object => abs path on disk
     52 
     53 	// writeMu serializes writing to the child process.
     54 	// It must never be held at the same time as mu.
     55 	writeMu sync.Mutex
     56 }
     57 
     58 // startCacheProg starts the prog binary (with optional space-separated flags)
     59 // and returns a Cache implementation that talks to it.
     60 //
     61 // It blocks a few seconds to wait for the child process to successfully start
     62 // and advertise its capabilities.
     63 func startCacheProg(progAndArgs string) Cache {
     64 	args := strings.Fields(progAndArgs)
     65 	var prog string
     66 	if len(args) > 0 {
     67 		prog = args[0]
     68 		args = args[1:]
     69 	}
     70 
     71 	ctx, ctxCancel := context.WithCancel(context.Background())
     72 
     73 	cmd := exec.CommandContext(ctx, prog, args...)
     74 	out, err := cmd.StdoutPipe()
     75 	if err != nil {
     76 		log.Fatalf("StdoutPipe to GOCACHEPROG: %v", err)
     77 	}
     78 	in, err := cmd.StdinPipe()
     79 	if err != nil {
     80 		log.Fatalf("StdinPipe to GOCACHEPROG: %v", err)
     81 	}
     82 	cmd.Stderr = os.Stderr
     83 	// On close, we cancel the context. Rather than killing the helper,
     84 	// close its stdin.
     85 	cmd.Cancel = in.Close
     86 
     87 	if err := cmd.Start(); err != nil {
     88 		log.Fatalf("error starting GOCACHEPROG program %q: %v", prog, err)
     89 	}
     90 
     91 	pc := &ProgCache{
     92 		ctx:          ctx,
     93 		ctxCancel:    ctxCancel,
     94 		cmd:          cmd,
     95 		stdout:       out,
     96 		stdin:        in,
     97 		bw:           bufio.NewWriter(in),
     98 		inFlight:     make(map[int64]chan<- *cacheprog.Response),
     99 		outputFile:   make(map[OutputID]string),
    100 		readLoopDone: make(chan struct{}),
    101 	}
    102 
    103 	// Register our interest in the initial protocol message from the child to
    104 	// us, saying what it can do.
    105 	capResc := make(chan *cacheprog.Response, 1)
    106 	pc.inFlight[0] = capResc
    107 
    108 	pc.jenc = json.NewEncoder(pc.bw)
    109 	go pc.readLoop(pc.readLoopDone)
    110 
    111 	// Give the child process a few seconds to report its capabilities. This
    112 	// should be instant and not require any slow work by the program.
    113 	timer := time.NewTicker(5 * time.Second)
    114 	defer timer.Stop()
    115 	for {
    116 		select {
    117 		case <-timer.C:
    118 			log.Printf("# still waiting for GOCACHEPROG %v ...", prog)
    119 		case capRes := <-capResc:
    120 			can := map[cacheprog.Cmd]bool{}
    121 			for _, cmd := range capRes.KnownCommands {
    122 				can[cmd] = true
    123 			}
    124 			if len(can) == 0 {
    125 				log.Fatalf("GOCACHEPROG %v declared no supported commands", prog)
    126 			}
    127 			pc.can = can
    128 			return pc
    129 		}
    130 	}
    131 }
    132 
    133 func (c *ProgCache) readLoop(readLoopDone chan<- struct{}) {
    134 	defer close(readLoopDone)
    135 	jd := json.NewDecoder(c.stdout)
    136 	for {
    137 		res := new(cacheprog.Response)
    138 		if err := jd.Decode(res); err != nil {
    139 			if c.closing.Load() {
    140 				c.mu.Lock()
    141 				for _, ch := range c.inFlight {
    142 					close(ch)
    143 				}
    144 				c.inFlight = nil
    145 				c.mu.Unlock()
    146 				return // quietly
    147 			}
    148 			if err == io.EOF {
    149 				c.mu.Lock()
    150 				inFlight := len(c.inFlight)
    151 				c.mu.Unlock()
    152 				log.Fatalf("GOCACHEPROG exited pre-Close with %v pending requests", inFlight)
    153 			}
    154 			log.Fatalf("error reading JSON from GOCACHEPROG: %v", err)
    155 		}
    156 		c.mu.Lock()
    157 		ch, ok := c.inFlight[res.ID]
    158 		delete(c.inFlight, res.ID)
    159 		c.mu.Unlock()
    160 		if ok {
    161 			ch <- res
    162 		} else {
    163 			log.Fatalf("GOCACHEPROG sent response for unknown request ID %v", res.ID)
    164 		}
    165 	}
    166 }
    167 
    168 var errCacheprogClosed = errors.New("GOCACHEPROG program closed unexpectedly")
    169 
    170 func (c *ProgCache) send(ctx context.Context, req *cacheprog.Request) (*cacheprog.Response, error) {
    171 	resc := make(chan *cacheprog.Response, 1)
    172 	if err := c.writeToChild(req, resc); err != nil {
    173 		return nil, err
    174 	}
    175 	select {
    176 	case res := <-resc:
    177 		if res == nil {
    178 			return nil, errCacheprogClosed
    179 		}
    180 		if res.Err != "" {
    181 			return nil, errors.New(res.Err)
    182 		}
    183 		return res, nil
    184 	case <-ctx.Done():
    185 		return nil, ctx.Err()
    186 	}
    187 }
    188 
    189 func (c *ProgCache) writeToChild(req *cacheprog.Request, resc chan<- *cacheprog.Response) (err error) {
    190 	c.mu.Lock()
    191 	if c.inFlight == nil {
    192 		c.mu.Unlock()
    193 		return errCacheprogClosed
    194 	}
    195 	c.nextID++
    196 	req.ID = c.nextID
    197 	c.inFlight[req.ID] = resc
    198 	c.mu.Unlock()
    199 
    200 	defer func() {
    201 		if err != nil {
    202 			c.mu.Lock()
    203 			if c.inFlight != nil {
    204 				delete(c.inFlight, req.ID)
    205 			}
    206 			c.mu.Unlock()
    207 		}
    208 	}()
    209 
    210 	c.writeMu.Lock()
    211 	defer c.writeMu.Unlock()
    212 
    213 	if err := c.jenc.Encode(req); err != nil {
    214 		return err
    215 	}
    216 	if err := c.bw.WriteByte('\n'); err != nil {
    217 		return err
    218 	}
    219 	if req.Body != nil && req.BodySize > 0 {
    220 		if err := c.bw.WriteByte('"'); err != nil {
    221 			return err
    222 		}
    223 		e := base64.NewEncoder(base64.StdEncoding, c.bw)
    224 		wrote, err := io.Copy(e, req.Body)
    225 		if err != nil {
    226 			return err
    227 		}
    228 		if err := e.Close(); err != nil {
    229 			return nil
    230 		}
    231 		if wrote != req.BodySize {
    232 			return fmt.Errorf("short write writing body to GOCACHEPROG for action %x, output %x: wrote %v; expected %v",
    233 				req.ActionID, req.OutputID, wrote, req.BodySize)
    234 		}
    235 		if _, err := c.bw.WriteString("\"\n"); err != nil {
    236 			return err
    237 		}
    238 	}
    239 	if err := c.bw.Flush(); err != nil {
    240 		return err
    241 	}
    242 	return nil
    243 }
    244 
    245 func (c *ProgCache) Get(a ActionID) (Entry, error) {
    246 	if !c.can[cacheprog.CmdGet] {
    247 		// They can't do a "get". Maybe they're a write-only cache.
    248 		return Entry{}, &entryNotFoundError{}
    249 	}
    250 	res, err := c.send(c.ctx, &cacheprog.Request{
    251 		Command:  cacheprog.CmdGet,
    252 		ActionID: a[:],
    253 	})
    254 	if err != nil {
    255 		return Entry{}, err
    256 	}
    257 	if res.Miss {
    258 		return Entry{}, &entryNotFoundError{}
    259 	}
    260 	e := Entry{
    261 		Size: res.Size,
    262 	}
    263 	if res.Time != nil {
    264 		e.Time = *res.Time
    265 	} else {
    266 		e.Time = time.Now()
    267 	}
    268 	if res.DiskPath == "" {
    269 		return Entry{}, &entryNotFoundError{errors.New("GOCACHEPROG didn't populate DiskPath on get hit")}
    270 	}
    271 	if copy(e.OutputID[:], res.OutputID) != len(res.OutputID) {
    272 		return Entry{}, &entryNotFoundError{errors.New("incomplete ProgResponse OutputID")}
    273 	}
    274 	c.noteOutputFile(e.OutputID, res.DiskPath)
    275 	return e, nil
    276 }
    277 
    278 func (c *ProgCache) noteOutputFile(o OutputID, diskPath string) {
    279 	c.mu.Lock()
    280 	defer c.mu.Unlock()
    281 	c.outputFile[o] = diskPath
    282 }
    283 
    284 func (c *ProgCache) OutputFile(o OutputID) string {
    285 	c.mu.Lock()
    286 	defer c.mu.Unlock()
    287 	return c.outputFile[o]
    288 }
    289 
    290 func (c *ProgCache) Put(a ActionID, file io.ReadSeeker) (_ OutputID, size int64, _ error) {
    291 	// Unwrap noVerifyReadSeeker; ProgCache doesn't use the verify mode.
    292 	if nv, ok := file.(noVerifyReadSeeker); ok {
    293 		file = nv.ReadSeeker
    294 	}
    295 
    296 	// Compute output ID.
    297 	h := sha256.New()
    298 	if _, err := file.Seek(0, 0); err != nil {
    299 		return OutputID{}, 0, err
    300 	}
    301 	size, err := io.Copy(h, file)
    302 	if err != nil {
    303 		return OutputID{}, 0, err
    304 	}
    305 	var out OutputID
    306 	h.Sum(out[:0])
    307 
    308 	if _, err := file.Seek(0, 0); err != nil {
    309 		return OutputID{}, 0, err
    310 	}
    311 
    312 	if !c.can[cacheprog.CmdPut] {
    313 		// Child is a read-only cache. Do nothing.
    314 		return out, size, nil
    315 	}
    316 
    317 	res, err := c.send(c.ctx, &cacheprog.Request{
    318 		Command:  cacheprog.CmdPut,
    319 		ActionID: a[:],
    320 		OutputID: out[:],
    321 		Body:     file,
    322 		BodySize: size,
    323 	})
    324 	if err != nil {
    325 		return OutputID{}, 0, err
    326 	}
    327 	if res.DiskPath == "" {
    328 		return OutputID{}, 0, errors.New("GOCACHEPROG didn't return DiskPath in put response")
    329 	}
    330 	c.noteOutputFile(out, res.DiskPath)
    331 	return out, size, err
    332 }
    333 
    334 func (c *ProgCache) Close() error {
    335 	c.closing.Store(true)
    336 	var err error
    337 
    338 	// First write a "close" message to the child so it can exit nicely
    339 	// and clean up if it wants. Only after that exchange do we cancel
    340 	// the context that kills the process.
    341 	if c.can[cacheprog.CmdClose] {
    342 		_, err = c.send(c.ctx, &cacheprog.Request{Command: cacheprog.CmdClose})
    343 		if errors.Is(err, errCacheprogClosed) {
    344 			// Allow the child to quit without responding to close.
    345 			err = nil
    346 		}
    347 	}
    348 	// Cancel the context, which will close the helper's stdin.
    349 	c.ctxCancel()
    350 	// Wait until the helper closes its stdout.
    351 	<-c.readLoopDone
    352 	return err
    353 }