| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218 |
- // Copyright 2019 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package attribute
-
- import (
- "bytes"
- "context"
- "fmt"
- "os"
- "path/filepath"
- "time"
-
- "code.gitea.io/gitea/modules/git"
- "code.gitea.io/gitea/modules/git/gitcmd"
- "code.gitea.io/gitea/modules/log"
- )
-
- // BatchChecker provides a reader for check-attribute content that can be long running
- type BatchChecker struct {
- attributesNum int
- repo *git.Repository
- stdinWriter *os.File
- stdOut *nulSeparatedAttributeWriter
- ctx context.Context
- cancel context.CancelFunc
- cmd *gitcmd.Command
- }
-
- // NewBatchChecker creates a check attribute reader for the current repository and provided commit ID
- // If treeish is empty, then it will use current working directory, otherwise it will use the provided treeish on the bare repo
- func NewBatchChecker(repo *git.Repository, treeish string, attributes []string) (checker *BatchChecker, returnedErr error) {
- ctx, cancel := context.WithCancel(repo.Ctx)
- defer func() {
- if returnedErr != nil {
- cancel()
- }
- }()
-
- cmd, envs, cleanup, err := checkAttrCommand(repo, treeish, nil, attributes)
- if err != nil {
- return nil, err
- }
- defer func() {
- if returnedErr != nil {
- cleanup()
- }
- }()
-
- cmd.AddArguments("--stdin")
-
- checker = &BatchChecker{
- attributesNum: len(attributes),
- repo: repo,
- ctx: ctx,
- cmd: cmd,
- cancel: func() {
- cancel()
- cleanup()
- },
- }
-
- stdinReader, stdinWriter, err := os.Pipe()
- if err != nil {
- return nil, err
- }
- checker.stdinWriter = stdinWriter
-
- lw := new(nulSeparatedAttributeWriter)
- lw.attributes = make(chan attributeTriple, len(attributes))
- lw.closed = make(chan struct{})
- checker.stdOut = lw
-
- go func() {
- defer func() {
- _ = stdinReader.Close()
- _ = lw.Close()
- }()
- stdErr := new(bytes.Buffer)
- err := cmd.Run(ctx, &gitcmd.RunOpts{
- Env: envs,
- Dir: repo.Path,
- Stdin: stdinReader,
- Stdout: lw,
- Stderr: stdErr,
- })
-
- if err != nil && !git.IsErrCanceledOrKilled(err) {
- log.Error("Attribute checker for commit %s exits with error: %v", treeish, err)
- }
- checker.cancel()
- }()
-
- return checker, nil
- }
-
- // CheckPath check attr for given path
- func (c *BatchChecker) CheckPath(path string) (rs *Attributes, err error) {
- defer func() {
- if err != nil && err != c.ctx.Err() {
- log.Error("Unexpected error when checking path %s in %s, error: %v", path, filepath.Base(c.repo.Path), err)
- }
- }()
-
- select {
- case <-c.ctx.Done():
- return nil, c.ctx.Err()
- default:
- }
-
- if _, err = c.stdinWriter.Write([]byte(path + "\x00")); err != nil {
- defer c.Close()
- return nil, err
- }
-
- reportTimeout := func() error {
- stdOutClosed := false
- select {
- case <-c.stdOut.closed:
- stdOutClosed = true
- default:
- }
- debugMsg := fmt.Sprintf("check path %q in repo %q", path, filepath.Base(c.repo.Path))
- debugMsg += fmt.Sprintf(", stdOut: tmp=%q, pos=%d, closed=%v", string(c.stdOut.tmp), c.stdOut.pos, stdOutClosed)
- if c.cmd != nil {
- debugMsg += fmt.Sprintf(", process state: %q", c.cmd.ProcessState())
- }
- _ = c.Close()
- return fmt.Errorf("CheckPath timeout: %s", debugMsg)
- }
-
- rs = NewAttributes()
- for i := 0; i < c.attributesNum; i++ {
- select {
- case <-time.After(5 * time.Second):
- // there is no "hang" problem now. This code is just used to catch other potential problems.
- return nil, reportTimeout()
- case attr, ok := <-c.stdOut.ReadAttribute():
- if !ok {
- return nil, c.ctx.Err()
- }
- rs.m[attr.Attribute] = Attribute(attr.Value)
- case <-c.ctx.Done():
- return nil, c.ctx.Err()
- }
- }
- return rs, nil
- }
-
- func (c *BatchChecker) Close() error {
- c.cancel()
- err := c.stdinWriter.Close()
- return err
- }
-
- type attributeTriple struct {
- Filename string
- Attribute string
- Value string
- }
-
- type nulSeparatedAttributeWriter struct {
- tmp []byte
- attributes chan attributeTriple
- closed chan struct{}
- working attributeTriple
- pos int
- }
-
- func (wr *nulSeparatedAttributeWriter) Write(p []byte) (n int, err error) {
- l, read := len(p), 0
-
- nulIdx := bytes.IndexByte(p, '\x00')
- for nulIdx >= 0 {
- wr.tmp = append(wr.tmp, p[:nulIdx]...)
- switch wr.pos {
- case 0:
- wr.working = attributeTriple{
- Filename: string(wr.tmp),
- }
- case 1:
- wr.working.Attribute = string(wr.tmp)
- case 2:
- wr.working.Value = string(wr.tmp)
- }
- wr.tmp = wr.tmp[:0]
- wr.pos++
- if wr.pos > 2 {
- wr.attributes <- wr.working
- wr.pos = 0
- }
- read += nulIdx + 1
- if l > read {
- p = p[nulIdx+1:]
- nulIdx = bytes.IndexByte(p, '\x00')
- } else {
- return l, nil
- }
- }
- wr.tmp = append(wr.tmp, p...)
- return l, nil
- }
-
- func (wr *nulSeparatedAttributeWriter) ReadAttribute() <-chan attributeTriple {
- return wr.attributes
- }
-
- func (wr *nulSeparatedAttributeWriter) Close() error {
- select {
- case <-wr.closed:
- return nil
- default:
- }
- close(wr.attributes)
- close(wr.closed)
- return nil
- }
|