golangci-lint/pkg/commands/executor.go
Romanos 1a2a672644
Allow for serializing multiple golangci-lint invocations (#1302)
By default, golangci-lint fails after five seconds if another instance
is running. It is possible to disable that, but the discussion around
whether the cache is safe to use concurrently is not exactly full of
confidence. Add a flag that allows golangci-lint to wait forever
instead of failing.

see #1301
2020-08-28 14:06:28 +08:00

250 lines
6.8 KiB
Go

package commands
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/fatih/color"
"github.com/gofrs/flock"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/golangci/golangci-lint/internal/cache"
"github.com/golangci/golangci-lint/internal/pkgcache"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
"github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/lint"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/timeutils"
)
type Executor struct {
rootCmd *cobra.Command
runCmd *cobra.Command
lintersCmd *cobra.Command
exitCode int
version, commit, date string
cfg *config.Config
log logutils.Log
reportData report.Data
DBManager *lintersdb.Manager
EnabledLintersSet *lintersdb.EnabledSet
contextLoader *lint.ContextLoader
goenv *goutil.Env
fileCache *fsutils.FileCache
lineCache *fsutils.LineCache
pkgCache *pkgcache.Cache
debugf logutils.DebugFunc
sw *timeutils.Stopwatch
loadGuard *load.Guard
flock *flock.Flock
}
func NewExecutor(version, commit, date string) *Executor {
startedAt := time.Now()
e := &Executor{
cfg: config.NewDefault(),
version: version,
commit: commit,
date: date,
DBManager: lintersdb.NewManager(nil, nil),
debugf: logutils.Debug("exec"),
}
e.debugf("Starting execution...")
e.log = report.NewLogWrapper(logutils.NewStderrLog(""), &e.reportData)
// to setup log level early we need to parse config from command line extra time to
// find `-v` option
commandLineCfg, err := e.getConfigForCommandLine()
if err != nil && err != pflag.ErrHelp {
e.log.Fatalf("Can't get config for command line: %s", err)
}
if commandLineCfg != nil {
logutils.SetupVerboseLog(e.log, commandLineCfg.Run.IsVerbose)
switch commandLineCfg.Output.Color {
case "always":
color.NoColor = false
case "never":
color.NoColor = true
case "auto":
// nothing
default:
e.log.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", commandLineCfg.Output.Color)
}
}
// init of commands must be done before config file reading because
// init sets config with the default values of flags
e.initRoot()
e.initRun()
e.initHelp()
e.initLinters()
e.initConfig()
e.initCompletion()
e.initVersion()
e.initCache()
// init e.cfg by values from config: flags parse will see these values
// like the default ones. It will overwrite them only if the same option
// is found in command-line: it's ok, command-line has higher priority.
r := config.NewFileReader(e.cfg, commandLineCfg, e.log.Child("config_reader"))
if err = r.Read(); err != nil {
e.log.Fatalf("Can't read config: %s", err)
}
// recreate after getting config
e.DBManager = lintersdb.NewManager(e.cfg, e.log).WithCustomLinters()
e.cfg.LintersSettings.Gocritic.InferEnabledChecks(e.log)
if err = e.cfg.LintersSettings.Gocritic.Validate(e.log); err != nil {
e.log.Fatalf("Invalid gocritic settings: %s", err)
}
// Slice options must be explicitly set for proper merging of config and command-line options.
fixSlicesFlags(e.runCmd.Flags())
fixSlicesFlags(e.lintersCmd.Flags())
e.EnabledLintersSet = lintersdb.NewEnabledSet(e.DBManager,
lintersdb.NewValidator(e.DBManager), e.log.Child("lintersdb"), e.cfg)
e.goenv = goutil.NewEnv(e.log.Child("goenv"))
e.fileCache = fsutils.NewFileCache()
e.lineCache = fsutils.NewLineCache(e.fileCache)
e.sw = timeutils.NewStopwatch("pkgcache", e.log.Child("stopwatch"))
e.pkgCache, err = pkgcache.NewCache(e.sw, e.log.Child("pkgcache"))
if err != nil {
e.log.Fatalf("Failed to build packages cache: %s", err)
}
e.loadGuard = load.NewGuard()
e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child("loader"), e.goenv,
e.lineCache, e.fileCache, e.pkgCache, e.loadGuard)
if err = e.initHashSalt(version); err != nil {
e.log.Fatalf("Failed to init hash salt: %s", err)
}
e.debugf("Initialized executor in %s", time.Since(startedAt))
return e
}
func (e *Executor) Execute() error {
return e.rootCmd.Execute()
}
func (e *Executor) initHashSalt(version string) error {
binSalt, err := computeBinarySalt(version)
if err != nil {
return errors.Wrap(err, "failed to calculate binary salt")
}
configSalt, err := computeConfigSalt(e.cfg)
if err != nil {
return errors.Wrap(err, "failed to calculate config salt")
}
var b bytes.Buffer
b.Write(binSalt)
b.Write(configSalt)
cache.SetSalt(b.Bytes())
return nil
}
func computeBinarySalt(version string) ([]byte, error) {
if version != "" && version != "(devel)" {
return []byte(version), nil
}
if logutils.HaveDebugTag("bin_salt") {
return []byte("debug"), nil
}
p, err := os.Executable()
if err != nil {
return nil, err
}
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
func computeConfigSalt(cfg *config.Config) ([]byte, error) {
// We don't hash all config fields to reduce meaningless cache
// invalidations. At least, it has a huge impact on tests speed.
lintersSettingsBytes, err := json.Marshal(cfg.LintersSettings)
if err != nil {
return nil, errors.Wrap(err, "failed to json marshal config linter settings")
}
var configData bytes.Buffer
configData.WriteString("linters-settings=")
configData.Write(lintersSettingsBytes)
configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ","))
h := sha256.New()
h.Write(configData.Bytes()) //nolint:errcheck
return h.Sum(nil), nil
}
func (e *Executor) acquireFileLock() bool {
if e.cfg.Run.AllowParallelRunners {
e.debugf("Parallel runners are allowed, no locking")
return true
}
lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock")
e.debugf("Locking on file %s...", lockFile)
f := flock.New(lockFile)
const retryDelay = time.Second
ctx := context.Background()
if !e.cfg.Run.AllowSerialRunners {
const totalTimeout = 5 * time.Second
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, totalTimeout)
defer cancel()
}
if ok, _ := f.TryLockContext(ctx, retryDelay); !ok {
return false
}
e.flock = f
return true
}
func (e *Executor) releaseFileLock() {
if e.cfg.Run.AllowParallelRunners {
return
}
if err := e.flock.Unlock(); err != nil {
e.debugf("Failed to unlock on file: %s", err)
}
if err := os.Remove(e.flock.Path()); err != nil {
e.debugf("Failed to remove lock file: %s", err)
}
}