stash/vendor/github.com/gobuffalo/buffalo-plugins/plugins/plugins.go
2019-02-09 04:32:50 -08:00

232 lines
5.1 KiB
Go

package plugins
import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gobuffalo/buffalo-plugins/plugins/plugdeps"
"github.com/gobuffalo/envy"
"github.com/gobuffalo/meta"
"github.com/karrick/godirwalk"
"github.com/markbates/oncer"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const timeoutEnv = "BUFFALO_PLUGIN_TIMEOUT"
var t = time.Second * 2
func timeout() time.Duration {
oncer.Do("plugins.timeout", func() {
rawTimeout, err := envy.MustGet(timeoutEnv)
if err == nil {
if parsed, err := time.ParseDuration(rawTimeout); err == nil {
t = parsed
} else {
logrus.Errorf("%q value is malformed assuming default %q: %v", timeoutEnv, t, err)
}
} else {
logrus.Debugf("%q not set, assuming default of %v", timeoutEnv, t)
}
})
return t
}
// List maps a Buffalo command to a slice of Command
type List map[string]Commands
var _list List
// Available plugins for the `buffalo` command.
// It will look in $GOPATH/bin and the `./plugins` directory.
// This can be changed by setting the $BUFFALO_PLUGIN_PATH
// environment variable.
//
// Requirements:
// * file/command must be executable
// * file/command must start with `buffalo-`
// * file/command must respond to `available` and return JSON of
// plugins.Commands{}
//
// Limit full path scan with direct plugin path
//
// If a file/command doesn't respond to being invoked with `available`
// within one second, buffalo will assume that it is unable to load. This
// can be changed by setting the $BUFFALO_PLUGIN_TIMEOUT environment
// variable. It must be set to a duration that `time.ParseDuration` can
// process.
func Available() (List, error) {
var err error
oncer.Do("plugins.Available", func() {
defer func() {
if err := saveCache(); err != nil {
logrus.Error(err)
}
}()
app := meta.New(".")
if plugdeps.On(app) {
_list, err = listPlugDeps(app)
return
}
paths := []string{"plugins"}
from, err := envy.MustGet("BUFFALO_PLUGIN_PATH")
if err != nil {
from, err = envy.MustGet("GOPATH")
if err != nil {
return
}
from = filepath.Join(from, "bin")
}
paths = append(paths, strings.Split(from, string(os.PathListSeparator))...)
list := List{}
for _, p := range paths {
if ignorePath(p) {
continue
}
if _, err := os.Stat(p); err != nil {
continue
}
err := godirwalk.Walk(p, &godirwalk.Options{
FollowSymbolicLinks: true,
Callback: func(path string, info *godirwalk.Dirent) error {
if err != nil {
// May indicate a permissions problem with the path, skip it
return nil
}
if info.IsDir() {
return nil
}
base := filepath.Base(path)
if strings.HasPrefix(base, "buffalo-") {
ctx, cancel := context.WithTimeout(context.Background(), timeout())
commands := askBin(ctx, path)
cancel()
for _, c := range commands {
bc := c.BuffaloCommand
if _, ok := list[bc]; !ok {
list[bc] = Commands{}
}
c.Binary = path
list[bc] = append(list[bc], c)
}
}
return nil
},
})
if err != nil {
return
}
}
_list = list
})
return _list, err
}
func askBin(ctx context.Context, path string) Commands {
start := time.Now()
defer func() {
logrus.Debugf("askBin %s=%.4f s", path, time.Since(start).Seconds())
}()
commands := Commands{}
defer func() {
addToCache(path, cachedPlugin{
Commands: commands,
})
}()
if cp, ok := findInCache(path); ok {
s := sum(path)
if s == cp.CheckSum {
logrus.Debugf("cache hit: %s", path)
commands = cp.Commands
return commands
}
}
logrus.Debugf("cache miss: %s", path)
if strings.HasPrefix(filepath.Base(path), "buffalo-no-sqlite") {
return commands
}
cmd := exec.CommandContext(ctx, path, "available")
bb := &bytes.Buffer{}
cmd.Stdout = bb
err := cmd.Run()
if err != nil {
return commands
}
msg := bb.String()
for len(msg) > 0 {
err = json.NewDecoder(strings.NewReader(msg)).Decode(&commands)
if err == nil {
return commands
}
msg = msg[1:]
}
logrus.Errorf("[PLUGIN] error decoding plugin %s: %s\n%s\n", path, err, msg)
return commands
}
func ignorePath(p string) bool {
p = strings.ToLower(p)
for _, x := range []string{`c:\windows`, `c:\program`} {
if strings.HasPrefix(p, x) {
return true
}
}
return false
}
func listPlugDeps(app meta.App) (List, error) {
list := List{}
plugs, err := plugdeps.List(app)
if err != nil {
return list, err
}
for _, p := range plugs.List() {
ctx, cancel := context.WithTimeout(context.Background(), timeout())
defer cancel()
bin := p.Binary
if len(p.Local) != 0 {
bin = p.Local
}
bin, err := LookPath(bin)
if err != nil {
if errors.Cause(err) != ErrPlugMissing {
return list, err
}
continue
}
commands := askBin(ctx, bin)
cancel()
for _, c := range commands {
bc := c.BuffaloCommand
if _, ok := list[bc]; !ok {
list[bc] = Commands{}
}
c.Binary = p.Binary
for _, pc := range p.Commands {
if c.Name == pc.Name {
c.Flags = pc.Flags
break
}
}
list[bc] = append(list[bc], c)
}
}
return list, nil
}