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 {
}
}
+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 {
}
select {
case res := <-resc:
+ if res == nil {
+ return nil, errCacheprogClosed
+ }
if res.Err != "" {
return nil, errors.New(res.Err)
}
func (c *ProgCache) writeToChild(req *cacheprog.Request, resc chan<- *cacheprog.Response) (err error) {
c.mu.Lock()
+ if c.inFlight == nil {
+ return errCacheprogClosed
+ }
c.nextID++
req.ID = c.nextID
c.inFlight[req.ID] = resc
defer func() {
if err != nil {
c.mu.Lock()
- delete(c.inFlight, req.ID)
+ if c.inFlight != nil {
+ delete(c.inFlight, req.ID)
+ }
c.mu.Unlock()
}
}()
// 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()
--- /dev/null
+[short] skip 'builds go programs'
+
+go build -o cacheprog$GOEXE cacheprog.go
+env GOCACHEPROG=$GOPATH/src/cacheprog$GOEXE
+
+# This should not deadlock
+go build simple.go
+! stderr 'cacheprog closed'
+
+-- simple.go --
+package main
+
+func main() {}
+-- cacheprog.go --
+// This is a minimal GOCACHEPROG program that doesn't respond to close.
+package main
+
+import (
+ "encoding/json"
+ "os"
+)
+
+func main() {
+ json.NewEncoder(os.Stdout).Encode(map[string][]string{"KnownCommands": {"close"}})
+ var res struct{}
+ json.NewDecoder(os.Stdin).Decode(&res)
+}
\ No newline at end of file