in lib/lib.go [269:417]
func (obj *Scanner) Scan(ctx context.Context, path safepath.Path, info *interfaces.Info) error {
errors := []error{}
mu := &sync.Mutex{} // guards list of errors
wg := &sync.WaitGroup{}
// TODO: we could switch and avoid doing this if we knew that
// zero backends were going to need it, but we know most will,
// so avoid optimizing early, and skip pre-checking for this.
var data []byte
var err error
if !info.FileInfo.IsDir() {
data, err = os.ReadFile(path.Path())
if err != nil {
return err // TODO: errwrap?
}
}
Loop:
for _, backend := range obj.Backends {
// Some backends aren't particularly well-behaved with
// regards to obeying the context cancellation signal.
// In an effort to short-circuit things if needed, we
// run a check ourselves and break out early if we see
// that we have cancelled early. This change improves
// cancellation latency significantly.
select {
case <-ctx.Done():
errors = append(errors, ctx.Err())
break Loop
default:
}
// TODO: add a counting semaphore if it's desired
wg.Add(1)
obj.wg.Add(1)
go func(backend interfaces.Backend) {
defer wg.Done()
defer obj.wg.Done()
if obj.Debug {
obj.Logf("scanning: %s", path)
}
var result *interfaces.Result
var err error
// XXX: cache lookups here?
// if x, ok := backend.(interfaces.CachedDataBackend); ok {
// result, err = x.LookupData(ctx, data, info)
// } else if x, ok := backend.(interfaces.CachedPathBackend); ok {
// result, err = x.LookupPath(ctx, path, info)
// }
// XXX: if err, look at cache policy, otherwise continue or err
if _, exists := obj.skipdirs[backend][info.UID]; info.FileInfo.IsDir() && exists {
if obj.Debug {
obj.Logf("skip dir: %s", path)
}
return
}
// XXX: wrap these in a helper function
if x, ok := backend.(interfaces.DataBackend); ok {
//if len(data) == 0 { // possible directory
// return // skip directories!
//}
result, err = x.ScanData(ctx, data, info)
} else if x, ok := backend.(interfaces.PathBackend); ok {
result, err = x.ScanPath(ctx, path, info)
} else {
return
}
// If a backend returns interfaces.SkipDir, then
// this is the signal that it doesn't need to
// return any different information in a deeper
// hierarchy of that scan. This is common when
// we have a backend that makes a whole
// repository or directory determination. In
// this case, store this rejection so that we
// don't wastefully call this backend again with
// a child path. The backend will likely want to
// also return a result with the SkipDir. It
// must not return SkipDir in response to a non
// directory path.
if err == interfaces.SkipDir {
// TODO: we could use a different mutex
// but I'm lazy and it won't help much!
obj.mu.Lock()
obj.skipdirs[backend][info.UID] = struct{}{}
obj.mu.Unlock()
} else if err != nil {
// XXX: ShutdownOnError and cancel the ctx?
mu.Lock()
errors = append(errors, err)
mu.Unlock()
return // goroutine ends
}
if result == nil { // skip nil results
return
}
// tag (annotate) the result
tagResultBackend(result, backend)
// store results
obj.mu.Lock()
if _, exists := obj.results[info.UID]; !exists {
obj.results[info.UID] = make(map[interfaces.Backend]*interfaces.Result)
}
if old, exists := obj.results[info.UID][backend]; exists {
// If we get a duplicate result, this can happen
// if there's a bug, or if we asked to scan the
// same thing more than once. As a result, run a
// cmp on both results, and if they're the same,
// then we can safely ignore this issue.
// XXX: can cached results cause this to fail?
if err := old.Cmp(result); err != nil {
e := errwrap.Wrapf(err, "duplicate result for path: %s", path)
mu.Lock()
errors = append(errors, e)
mu.Unlock()
return // goroutine ends
}
}
obj.results[info.UID][backend] = result
obj.mu.Unlock()
// XXX: cache results
// if x, ok := backend.(interfaces.CachedDataBackend); ok {
// result, err = x.LookupData(ctx, data, info)
// } else if x, ok := backend.(interfaces.CachedPathBackend); ok {
// result, err = x.LookupPath(ctx, path, info)
// }
}(backend)
}
wg.Wait()
if len(errors) > 0 {
var ea error
for _, e := range errors {
ea = errwrap.Append(ea, e)
}
return errwrap.Wrapf(ea, "scan func errored")
}
return nil
}