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 }