github.com/JarrahG/buffalocli@v0.0.0-20230801092127-b85bfd5d395a/internal/plugins/plugins.go (about) 1 package plugins 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "io/fs" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/JarrahG/buffalocli/internal/plugins/plugdeps" 17 "github.com/gobuffalo/envy" 18 "github.com/gobuffalo/meta" 19 "github.com/sirupsen/logrus" 20 ) 21 22 const timeoutEnv = "BUFFALO_PLUGIN_TIMEOUT" 23 24 var t = time.Second * 2 25 var initTimeoutOnce sync.Once 26 27 func timeout() time.Duration { 28 initTimeoutOnce.Do(func() { 29 rawTimeout, err := envy.MustGet(timeoutEnv) 30 if err == nil { 31 if parsed, err := time.ParseDuration(rawTimeout); err == nil { 32 t = parsed 33 } else { 34 logrus.Errorf("%q value is malformed assuming default %q: %v", timeoutEnv, t, err) 35 } 36 } else { 37 logrus.Debugf("%q not set, assuming default of %v", timeoutEnv, t) 38 } 39 }) 40 return t 41 } 42 43 // List maps a Buffalo command to a slice of Command 44 type List map[string]Commands 45 46 var _list List 47 var scanPlugsOnce sync.Once 48 49 // Available plugins for the `buffalo` command. 50 // It will look in $GOPATH/bin and the `./plugins` directory. 51 // This can be changed by setting the $BUFFALO_PLUGIN_PATH 52 // environment variable. 53 // 54 // Requirements: 55 // * file/command must be executable 56 // * file/command must start with `buffalo-` 57 // * file/command must respond to `available` and return JSON of 58 // plugins.Commands{} 59 // 60 // Limit full path scan with direct plugin path 61 // 62 // If a file/command doesn't respond to being invoked with `available` 63 // within one second, buffalo will assume that it is unable to load. This 64 // can be changed by setting the $BUFFALO_PLUGIN_TIMEOUT environment 65 // variable. It must be set to a duration that `time.ParseDuration` can 66 // process. 67 func Available() (List, error) { 68 var err error 69 scanPlugsOnce.Do(func() { 70 defer func() { 71 if err := saveCache(); err != nil { 72 logrus.Error(err) 73 } 74 }() 75 76 app := meta.New(".") 77 78 if plugdeps.On(app) { 79 _list, err = listPlugDeps(app) 80 return 81 } 82 83 paths := []string{"plugins"} 84 85 from, err := envy.MustGet("BUFFALO_PLUGIN_PATH") 86 if err != nil { 87 from, err = envy.MustGet("GOPATH") 88 if err != nil { 89 return 90 } 91 from = filepath.Join(from, "bin") 92 } 93 94 paths = append(paths, strings.Split(from, string(os.PathListSeparator))...) 95 96 list := List{} 97 for _, p := range paths { 98 if ignorePath(p) { 99 continue 100 } 101 if _, err := os.Stat(p); err != nil { 102 continue 103 } 104 105 err := fs.WalkDir(os.DirFS(p), ".", func(path string, d fs.DirEntry, err error) error { 106 if err != nil { 107 return nil 108 } 109 if d.IsDir() { 110 return nil 111 } 112 113 base := filepath.Base(path) 114 if strings.HasPrefix(base, "buffalo-") && !strings.HasPrefix(base, "buffalo-plugins") { 115 ctx, cancel := context.WithTimeout(context.Background(), timeout()) 116 commands := askBin(ctx, path) 117 cancel() 118 for _, c := range commands { 119 bc := c.BuffaloCommand 120 if _, ok := list[bc]; !ok { 121 list[bc] = Commands{} 122 } 123 c.Binary = path 124 list[bc] = append(list[bc], c) 125 } 126 } 127 return nil 128 }) 129 if err != nil { 130 return 131 } 132 } 133 _list = list 134 }) 135 return _list, err 136 } 137 138 func askBin(ctx context.Context, path string) Commands { 139 start := time.Now() 140 defer func() { 141 logrus.Debugf("askBin %s=%.4f s", path, time.Since(start).Seconds()) 142 }() 143 144 commands := Commands{} 145 if cp, ok := findInCache(path); ok { 146 s := sum(path) 147 if s == cp.CheckSum { 148 logrus.Debugf("cache hit: %s", path) 149 commands = cp.Commands 150 return commands 151 } 152 } 153 logrus.Debugf("cache miss: %s", path) 154 if strings.HasPrefix(filepath.Base(path), "buffalo-no-sqlite") { 155 return commands 156 } 157 158 cmd := exec.CommandContext(ctx, path, "available") 159 bb := &bytes.Buffer{} 160 cmd.Stdout = bb 161 err := cmd.Run() 162 if err != nil { 163 logrus.Errorf("[PLUGIN] error loading plugin %s: %s\n", path, err) 164 return commands 165 } 166 167 msg := bb.String() 168 for len(msg) > 0 { 169 err = json.NewDecoder(strings.NewReader(msg)).Decode(&commands) 170 if err == nil { 171 addToCache(path, cachedPlugin{ 172 Commands: commands, 173 }) 174 return commands 175 } 176 msg = msg[1:] 177 } 178 logrus.Errorf("[PLUGIN] error decoding plugin %s: %s\n%s\n", path, err, msg) 179 return commands 180 } 181 182 func ignorePath(p string) bool { 183 p = strings.ToLower(p) 184 for _, x := range []string{`c:\windows`, `c:\program`} { 185 if strings.HasPrefix(p, x) { 186 return true 187 } 188 } 189 return false 190 } 191 192 func listPlugDeps(app meta.App) (List, error) { 193 list := List{} 194 plugs, err := plugdeps.List(app) 195 if err != nil { 196 return list, err 197 } 198 for _, p := range plugs.List() { 199 ctx, cancel := context.WithTimeout(context.Background(), timeout()) 200 defer cancel() 201 bin := p.Binary 202 if len(p.Local) != 0 { 203 bin = p.Local 204 } 205 bin, err := LookPath(bin) 206 if err != nil { 207 if !errors.Is(err, ErrPlugMissing) { 208 return list, err 209 } 210 continue 211 } 212 commands := askBin(ctx, bin) 213 cancel() 214 for _, c := range commands { 215 bc := c.BuffaloCommand 216 if _, ok := list[bc]; !ok { 217 list[bc] = Commands{} 218 } 219 c.Binary = p.Binary 220 for _, pc := range p.Commands { 221 if c.Name == pc.Name { 222 c.Flags = pc.Flags 223 break 224 } 225 } 226 list[bc] = append(list[bc], c) 227 } 228 } 229 return list, nil 230 }