]> git.lizzy.rs Git - micro.git/blob - cmd/micro/pluginmanager.go
Add more descriptive error messages for plugin installation failures
[micro.git] / cmd / micro / pluginmanager.go
1 package main
2
3 import (
4         "archive/zip"
5         "bytes"
6         "fmt"
7         "io"
8         "io/ioutil"
9         "net/http"
10         "os"
11         "path/filepath"
12         "sort"
13         "strings"
14         "sync"
15
16         "github.com/blang/semver"
17         "github.com/yuin/gopher-lua"
18         "github.com/zyedidia/json5/encoding/json5"
19 )
20
21 var (
22         allPluginPackages PluginPackages = nil
23 )
24
25 // CorePluginName is a plugin dependency name for the micro core.
26 const CorePluginName = "micro"
27
28 // PluginChannel contains an url to a json list of PluginRepository
29 type PluginChannel string
30
31 // PluginChannels is a slice of PluginChannel
32 type PluginChannels []PluginChannel
33
34 // PluginRepository contains an url to json file containing PluginPackages
35 type PluginRepository string
36
37 // PluginPackage contains the meta-data of a plugin and all available versions
38 type PluginPackage struct {
39         Name        string
40         Description string
41         Author      string
42         Tags        []string
43         Versions    PluginVersions
44 }
45
46 // PluginPackages is a list of PluginPackage instances.
47 type PluginPackages []*PluginPackage
48
49 // PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies.
50 type PluginVersion struct {
51         pack    *PluginPackage
52         Version semver.Version
53         Url     string
54         Require PluginDependencies
55 }
56
57 // PluginVersions is a slice of PluginVersion
58 type PluginVersions []*PluginVersion
59
60 // PluginDenendency descripes a dependency to another plugin or micro itself.
61 type PluginDependency struct {
62         Name  string
63         Range semver.Range
64 }
65
66 // PluginDependencies is a slice of PluginDependency
67 type PluginDependencies []*PluginDependency
68
69 func (pp *PluginPackage) String() string {
70         buf := new(bytes.Buffer)
71         buf.WriteString("Plugin: ")
72         buf.WriteString(pp.Name)
73         buf.WriteRune('\n')
74         if pp.Author != "" {
75                 buf.WriteString("Author: ")
76                 buf.WriteString(pp.Author)
77                 buf.WriteRune('\n')
78         }
79         if pp.Description != "" {
80                 buf.WriteRune('\n')
81                 buf.WriteString(pp.Description)
82         }
83         return buf.String()
84 }
85
86 func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages {
87         wgQuery := new(sync.WaitGroup)
88         wgQuery.Add(count)
89
90         results := make(chan PluginPackages)
91
92         wgDone := new(sync.WaitGroup)
93         wgDone.Add(1)
94         var packages PluginPackages
95         for i := 0; i < count; i++ {
96                 go func(i int) {
97                         results <- fetcher(i)
98                         wgQuery.Done()
99                 }(i)
100         }
101         go func() {
102                 packages = make(PluginPackages, 0)
103                 for res := range results {
104                         packages = append(packages, res...)
105                 }
106                 wgDone.Done()
107         }()
108         wgQuery.Wait()
109         close(results)
110         wgDone.Wait()
111         return packages
112 }
113
114 // Fetch retrieves all available PluginPackages from the given channels
115 func (pc PluginChannels) Fetch() PluginPackages {
116         return fetchAllSources(len(pc), func(i int) PluginPackages {
117                 return pc[i].Fetch()
118         })
119 }
120
121 // Fetch retrieves all available PluginPackages from the given channel
122 func (pc PluginChannel) Fetch() PluginPackages {
123         messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc)))
124         resp, err := http.Get(string(pc))
125         if err != nil {
126                 TermMessage("Failed to query plugin channel:\n", err)
127                 return PluginPackages{}
128         }
129         defer resp.Body.Close()
130         decoder := json5.NewDecoder(resp.Body)
131
132         var repositories []PluginRepository
133         if err := decoder.Decode(&repositories); err != nil {
134                 TermMessage("Failed to decode channel data:\n", err)
135                 return PluginPackages{}
136         }
137         return fetchAllSources(len(repositories), func(i int) PluginPackages {
138                 return repositories[i].Fetch()
139         })
140 }
141
142 // Fetch retrieves all available PluginPackages from the given repository
143 func (pr PluginRepository) Fetch() PluginPackages {
144         messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr)))
145         resp, err := http.Get(string(pr))
146         if err != nil {
147                 TermMessage("Failed to query plugin repository:\n", err)
148                 return PluginPackages{}
149         }
150         defer resp.Body.Close()
151         decoder := json5.NewDecoder(resp.Body)
152
153         var plugins PluginPackages
154         if err := decoder.Decode(&plugins); err != nil {
155                 TermMessage("Failed to decode repository data:\n", err)
156                 return PluginPackages{}
157         }
158         if len(plugins) > 0 {
159                 return PluginPackages{plugins[0]}
160         }
161         return nil
162         // return plugins
163 }
164
165 // UnmarshalJSON unmarshals raw json to a PluginVersion
166 func (pv *PluginVersion) UnmarshalJSON(data []byte) error {
167         var values struct {
168                 Version semver.Version
169                 Url     string
170                 Require map[string]string
171         }
172
173         if err := json5.Unmarshal(data, &values); err != nil {
174                 return err
175         }
176         pv.Version = values.Version
177         pv.Url = values.Url
178         pv.Require = make(PluginDependencies, 0)
179
180         for k, v := range values.Require {
181                 // don't add the dependency if it's the core and
182                 // we have a unknown version number.
183                 // in that case just accept that dependency (which equals to not adding it.)
184                 if k != CorePluginName || !isUnknownCoreVersion() {
185                         if vRange, err := semver.ParseRange(v); err == nil {
186                                 pv.Require = append(pv.Require, &PluginDependency{k, vRange})
187                         }
188                 }
189         }
190         return nil
191 }
192
193 // UnmarshalJSON unmarshals raw json to a PluginPackage
194 func (pp *PluginPackage) UnmarshalJSON(data []byte) error {
195         var values struct {
196                 Name        string
197                 Description string
198                 Author      string
199                 Tags        []string
200                 Versions    PluginVersions
201         }
202         if err := json5.Unmarshal(data, &values); err != nil {
203                 return err
204         }
205         pp.Name = values.Name
206         pp.Description = values.Description
207         pp.Author = values.Author
208         pp.Tags = values.Tags
209         pp.Versions = values.Versions
210         for _, v := range pp.Versions {
211                 v.pack = pp
212         }
213         return nil
214 }
215
216 // GetAllPluginPackages gets all PluginPackages which may be available.
217 func GetAllPluginPackages() PluginPackages {
218         if allPluginPackages == nil {
219                 getOption := func(name string) []string {
220                         data := GetOption(name)
221                         if strs, ok := data.([]string); ok {
222                                 return strs
223                         }
224                         if ifs, ok := data.([]interface{}); ok {
225                                 result := make([]string, len(ifs))
226                                 for i, urlIf := range ifs {
227                                         if url, ok := urlIf.(string); ok {
228                                                 result[i] = url
229                                         } else {
230                                                 return nil
231                                         }
232                                 }
233                                 return result
234                         }
235                         return nil
236                 }
237
238                 channels := PluginChannels{}
239                 for _, url := range getOption("pluginchannels") {
240                         channels = append(channels, PluginChannel(url))
241                 }
242                 repos := []PluginRepository{}
243                 for _, url := range getOption("pluginrepos") {
244                         repos = append(repos, PluginRepository(url))
245                 }
246                 allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages {
247                         if i == 0 {
248                                 return channels.Fetch()
249                         } else {
250                                 return repos[i-1].Fetch()
251                         }
252                 })
253         }
254         return allPluginPackages
255 }
256
257 func (pv PluginVersions) find(ppName string) *PluginVersion {
258         for _, v := range pv {
259                 if v.pack.Name == ppName {
260                         return v
261                 }
262         }
263         return nil
264 }
265
266 // Len returns the number of pluginversions in this slice
267 func (pv PluginVersions) Len() int {
268         return len(pv)
269 }
270
271 // Swap two entries of the slice
272 func (pv PluginVersions) Swap(i, j int) {
273         pv[i], pv[j] = pv[j], pv[i]
274 }
275
276 // Less returns true if the version at position i is greater then the version at position j (used for sorting)
277 func (s PluginVersions) Less(i, j int) bool {
278         return s[i].Version.GT(s[j].Version)
279 }
280
281 // Match returns true if the package matches a given search text
282 func (pp PluginPackage) Match(text string) bool {
283         text = strings.ToLower(text)
284         for _, t := range pp.Tags {
285                 if strings.ToLower(t) == text {
286                         return true
287                 }
288         }
289         if strings.Contains(strings.ToLower(pp.Name), text) {
290                 return true
291         }
292
293         if strings.Contains(strings.ToLower(pp.Description), text) {
294                 return true
295         }
296
297         return false
298 }
299
300 // IsInstallable returns true if the package can be installed.
301 func (pp PluginPackage) IsInstallable() error {
302         _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
303                 &PluginDependency{
304                         Name:  pp.Name,
305                         Range: semver.Range(func(v semver.Version) bool { return true }),
306                 }})
307         return err
308 }
309
310 // SearchPlugin retrieves a list of all PluginPackages which match the given search text and
311 // could be or are already installed
312 func SearchPlugin(texts []string) (plugins PluginPackages) {
313         plugins = make(PluginPackages, 0)
314
315 pluginLoop:
316         for _, pp := range GetAllPluginPackages() {
317                 for _, text := range texts {
318                         if !pp.Match(text) {
319                                 continue pluginLoop
320                         }
321                 }
322
323                 if err := pp.IsInstallable(); err == nil {
324                         plugins = append(plugins, pp)
325                 }
326         }
327         return
328 }
329
330 func isUnknownCoreVersion() bool {
331         _, err := semver.ParseTolerant(Version)
332         return err != nil
333 }
334
335 func newStaticPluginVersion(name, version string) *PluginVersion {
336         vers, err := semver.ParseTolerant(version)
337
338         if err != nil {
339                 if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil {
340                         vers = semver.MustParse("0.0.0-unknown")
341                 }
342         }
343         pl := &PluginPackage{
344                 Name: name,
345         }
346         pv := &PluginVersion{
347                 pack:    pl,
348                 Version: vers,
349         }
350         pl.Versions = PluginVersions{pv}
351         return pv
352 }
353
354 // GetInstalledVersions returns a list of all currently installed plugins including an entry for
355 // micro itself. This can be used to resolve dependencies.
356 func GetInstalledVersions(withCore bool) PluginVersions {
357         result := PluginVersions{}
358         if withCore {
359                 result = append(result, newStaticPluginVersion(CorePluginName, Version))
360         }
361
362         for _, name := range loadedPlugins {
363                 version := GetInstalledPluginVersion(name)
364                 if pv := newStaticPluginVersion(name, version); pv != nil {
365                         result = append(result, pv)
366                 }
367         }
368
369         return result
370 }
371
372 // GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin
373 func GetInstalledPluginVersion(name string) string {
374         plugin := L.GetGlobal(name)
375         if plugin != lua.LNil {
376                 version := L.GetField(plugin, "VERSION")
377                 if str, ok := version.(lua.LString); ok {
378                         return string(str)
379
380                 }
381         }
382         return ""
383 }
384
385 func (pv *PluginVersion) DownloadAndInstall() error {
386         messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url))
387         resp, err := http.Get(pv.Url)
388         if err != nil {
389                 return err
390         }
391         defer resp.Body.Close()
392         data, err := ioutil.ReadAll(resp.Body)
393         if err != nil {
394                 return err
395         }
396         zipbuf := bytes.NewReader(data)
397         z, err := zip.NewReader(zipbuf, zipbuf.Size())
398         if err != nil {
399                 return err
400         }
401         targetDir := filepath.Join(configDir, "plugins", pv.pack.Name)
402         dirPerm := os.FileMode(0755)
403         if err = os.MkdirAll(targetDir, dirPerm); err != nil {
404                 return err
405         }
406
407         // Check if all files in zip are in the same directory.
408         // this might be the case if the plugin zip contains the whole plugin dir
409         // instead of its content.
410         var prefix string
411         allPrefixed := false
412         for i, f := range z.File {
413                 parts := strings.Split(f.Name, "/")
414                 if i == 0 {
415                         prefix = parts[0]
416                 } else if parts[0] != prefix {
417                         allPrefixed = false
418                         break
419                 } else {
420                         // switch to true since we have at least a second file
421                         allPrefixed = true
422                 }
423         }
424
425         for _, f := range z.File {
426                 parts := strings.Split(f.Name, "/")
427                 if allPrefixed {
428                         parts = parts[1:]
429                 }
430
431                 targetName := filepath.Join(targetDir, filepath.Join(parts...))
432                 if f.FileInfo().IsDir() {
433                         if err := os.MkdirAll(targetName, dirPerm); err != nil {
434                                 return err
435                         }
436                 } else {
437                         content, err := f.Open()
438                         if err != nil {
439                                 return err
440                         }
441                         defer content.Close()
442                         if target, err := os.Create(targetName); err != nil {
443                                 return err
444                         } else {
445                                 defer target.Close()
446                                 if _, err = io.Copy(target, content); err != nil {
447                                         return err
448                                 }
449                         }
450                 }
451         }
452         return nil
453 }
454
455 func (pl PluginPackages) Get(name string) *PluginPackage {
456         for _, p := range pl {
457                 if p.Name == name {
458                         return p
459                 }
460         }
461         return nil
462 }
463
464 func (pl PluginPackages) GetAllVersions(name string) PluginVersions {
465         result := make(PluginVersions, 0)
466         p := pl.Get(name)
467         if p != nil {
468                 for _, v := range p.Versions {
469                         result = append(result, v)
470                 }
471         }
472         return result
473 }
474
475 func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies {
476         m := make(map[string]*PluginDependency)
477         for _, r := range req {
478                 m[r.Name] = r
479         }
480         for _, o := range other {
481                 cur, ok := m[o.Name]
482                 if ok {
483                         m[o.Name] = &PluginDependency{
484                                 o.Name,
485                                 o.Range.AND(cur.Range),
486                         }
487                 } else {
488                         m[o.Name] = o
489                 }
490         }
491         result := make(PluginDependencies, 0, len(m))
492         for _, v := range m {
493                 result = append(result, v)
494         }
495         return result
496 }
497
498 func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) {
499         if len(open) == 0 {
500                 return selectedVersions, nil
501         }
502         currentRequirement, stillOpen := open[0], open[1:]
503         if currentRequirement != nil {
504                 if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil {
505                         if currentRequirement.Range(selVersion.Version) {
506                                 return all.Resolve(selectedVersions, stillOpen)
507                         }
508                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
509                 } else {
510                         availableVersions := all.GetAllVersions(currentRequirement.Name)
511                         sort.Sort(availableVersions)
512
513                         for _, version := range availableVersions {
514                                 if currentRequirement.Range(version.Version) {
515                                         resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require))
516
517                                         if err == nil {
518                                                 return resolved, nil
519                                         }
520                                 }
521                         }
522                         return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name)
523                 }
524         } else {
525                 return selectedVersions, nil
526         }
527 }
528
529 func (versions PluginVersions) install() {
530         anyInstalled := false
531         currentlyInstalled := GetInstalledVersions(true)
532
533         for _, sel := range versions {
534                 if sel.pack.Name != CorePluginName {
535                         shouldInstall := true
536                         if pv := currentlyInstalled.find(sel.pack.Name); pv != nil {
537                                 if pv.Version.NE(sel.Version) {
538                                         messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name))
539                                         UninstallPlugin(sel.pack.Name)
540                                 } else {
541                                         shouldInstall = false
542                                 }
543                         }
544
545                         if shouldInstall {
546                                 if err := sel.DownloadAndInstall(); err != nil {
547                                         messenger.Error(err)
548                                         return
549                                 }
550                                 anyInstalled = true
551                         }
552                 }
553         }
554         if anyInstalled {
555                 messenger.Message("One or more plugins installed. Please restart micro.")
556         } else {
557                 messenger.AddLog("Nothing to install / update")
558         }
559 }
560
561 // UninstallPlugin deletes the plugin folder of the given plugin
562 func UninstallPlugin(name string) {
563         if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil {
564                 messenger.Error(err)
565         }
566 }
567
568 func (pl PluginPackage) Install() {
569         selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{
570                 &PluginDependency{
571                         Name:  pl.Name,
572                         Range: semver.Range(func(v semver.Version) bool { return true }),
573                 }})
574         if err != nil {
575                 TermMessage(err)
576                 return
577         }
578         selected.install()
579 }
580
581 func UpdatePlugins(plugins []string) {
582         // if no plugins are specified, update all installed plugins.
583         if len(plugins) == 0 {
584                 plugins = loadedPlugins
585         }
586
587         messenger.AddLog("Checking for plugin updates")
588         microVersion := PluginVersions{
589                 newStaticPluginVersion(CorePluginName, Version),
590         }
591
592         var updates = make(PluginDependencies, 0)
593         for _, name := range plugins {
594                 pv := GetInstalledPluginVersion(name)
595                 r, err := semver.ParseRange(">=" + pv) // Try to get newer versions.
596                 if err == nil {
597                         updates = append(updates, &PluginDependency{
598                                 Name:  name,
599                                 Range: r,
600                         })
601                 }
602         }
603
604         selected, err := GetAllPluginPackages().Resolve(microVersion, updates)
605         if err != nil {
606                 TermMessage(err)
607                 return
608         }
609         selected.install()
610 }