mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
4.8 KiB
216 lines
4.8 KiB
// 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.WithEnv(envs). |
|
WithDir(repo.Path). |
|
WithStdin(stdinReader). |
|
WithStdout(lw). |
|
WithStderr(stdErr). |
|
Run(ctx) |
|
|
|
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 |
|
}
|
|
|