X-Git-Url: https://git.lizzy.rs/?a=blobdiff_plain;f=cmd%2Fmicro%2Fpluginmanager.go;h=0703a62f5d1d4339aeceb35728cf5281a50a1eb3;hb=d49e366413dc8956f6b62734a6331df6632672fe;hp=25e0890df874c6d72a11fd18e0c82b1f5539bf45;hpb=567faeb07e1511af0784276d10856598fbf77511;p=micro.git diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 25e0890d..0703a62f 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -3,26 +3,38 @@ package main import ( "archive/zip" "bytes" - "encoding/json" "fmt" "io" "io/ioutil" "net/http" "os" "path/filepath" - "regexp" "sort" "strings" "sync" "github.com/blang/semver" "github.com/yuin/gopher-lua" + "github.com/zyedidia/json5/encoding/json5" ) -var Repositories []PluginRepository = []PluginRepository{} +var ( + allPluginPackages PluginPackages +) + +// CorePluginName is a plugin dependency name for the micro core. +const CorePluginName = "micro" + +// PluginChannel contains an url to a json list of PluginRepository +type PluginChannel string +// PluginChannels is a slice of PluginChannel +type PluginChannels []PluginChannel + +// PluginRepository contains an url to json file containing PluginPackages type PluginRepository string +// PluginPackage contains the meta-data of a plugin and all available versions type PluginPackage struct { Name string Description string @@ -31,22 +43,126 @@ type PluginPackage struct { Versions PluginVersions } +// PluginPackages is a list of PluginPackage instances. type PluginPackages []*PluginPackage +// PluginVersion descripes a version of a PluginPackage. Containing a version, download url and also dependencies. type PluginVersion struct { pack *PluginPackage Version semver.Version Url string Require PluginDependencies } + +// PluginVersions is a slice of PluginVersion type PluginVersions []*PluginVersion +// PluginDependency descripes a dependency to another plugin or micro itself. type PluginDependency struct { Name string Range semver.Range } + +// PluginDependencies is a slice of PluginDependency type PluginDependencies []*PluginDependency +func (pp *PluginPackage) String() string { + buf := new(bytes.Buffer) + buf.WriteString("Plugin: ") + buf.WriteString(pp.Name) + buf.WriteRune('\n') + if pp.Author != "" { + buf.WriteString("Author: ") + buf.WriteString(pp.Author) + buf.WriteRune('\n') + } + if pp.Description != "" { + buf.WriteRune('\n') + buf.WriteString(pp.Description) + } + return buf.String() +} + +func fetchAllSources(count int, fetcher func(i int) PluginPackages) PluginPackages { + wgQuery := new(sync.WaitGroup) + wgQuery.Add(count) + + results := make(chan PluginPackages) + + wgDone := new(sync.WaitGroup) + wgDone.Add(1) + var packages PluginPackages + for i := 0; i < count; i++ { + go func(i int) { + results <- fetcher(i) + wgQuery.Done() + }(i) + } + go func() { + packages = make(PluginPackages, 0) + for res := range results { + packages = append(packages, res...) + } + wgDone.Done() + }() + wgQuery.Wait() + close(results) + wgDone.Wait() + return packages +} + +// Fetch retrieves all available PluginPackages from the given channels +func (pc PluginChannels) Fetch() PluginPackages { + return fetchAllSources(len(pc), func(i int) PluginPackages { + return pc[i].Fetch() + }) +} + +// Fetch retrieves all available PluginPackages from the given channel +func (pc PluginChannel) Fetch() PluginPackages { + // messenger.AddLog(fmt.Sprintf("Fetching channel: %q", string(pc))) + resp, err := http.Get(string(pc)) + if err != nil { + TermMessage("Failed to query plugin channel:\n", err) + return PluginPackages{} + } + defer resp.Body.Close() + decoder := json5.NewDecoder(resp.Body) + + var repositories []PluginRepository + if err := decoder.Decode(&repositories); err != nil { + TermMessage("Failed to decode channel data:\n", err) + return PluginPackages{} + } + return fetchAllSources(len(repositories), func(i int) PluginPackages { + return repositories[i].Fetch() + }) +} + +// Fetch retrieves all available PluginPackages from the given repository +func (pr PluginRepository) Fetch() PluginPackages { + // messenger.AddLog(fmt.Sprintf("Fetching repository: %q", string(pr))) + resp, err := http.Get(string(pr)) + if err != nil { + TermMessage("Failed to query plugin repository:\n", err) + return PluginPackages{} + } + defer resp.Body.Close() + decoder := json5.NewDecoder(resp.Body) + + var plugins PluginPackages + if err := decoder.Decode(&plugins); err != nil { + TermMessage("Failed to decode repository data:\n", err) + return PluginPackages{} + } + if len(plugins) > 0 { + return PluginPackages{plugins[0]} + } + return nil + // return plugins +} + +// UnmarshalJSON unmarshals raw json to a PluginVersion func (pv *PluginVersion) UnmarshalJSON(data []byte) error { var values struct { Version semver.Version @@ -54,7 +170,7 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { Require map[string]string } - if err := json.Unmarshal(data, &values); err != nil { + if err := json5.Unmarshal(data, &values); err != nil { return err } pv.Version = values.Version @@ -62,21 +178,19 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { pv.Require = make(PluginDependencies, 0) for k, v := range values.Require { - if vRange, err := semver.ParseRange(v); err == nil { - pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + // don't add the dependency if it's the core and + // we have a unknown version number. + // in that case just accept that dependency (which equals to not adding it.) + if k != CorePluginName || !isUnknownCoreVersion() { + if vRange, err := semver.ParseRange(v); err == nil { + pv.Require = append(pv.Require, &PluginDependency{k, vRange}) + } } } return nil } -func (pv *PluginVersion) String() string { - return fmt.Sprintf("%s (%s)", pv.pack.Name, pv.Version) -} - -func (pd *PluginDependency) String() string { - return pd.Name -} - +// UnmarshalJSON unmarshals raw json to a PluginPackage func (pp *PluginPackage) UnmarshalJSON(data []byte) error { var values struct { Name string @@ -85,7 +199,7 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { Tags []string Versions PluginVersions } - if err := json.Unmarshal(data, &values); err != nil { + if err := json5.Unmarshal(data, &values); err != nil { return err } pp.Name = values.Name @@ -99,176 +213,244 @@ func (pp *PluginPackage) UnmarshalJSON(data []byte) error { return nil } -func (pv PluginVersions) Find(name string) *PluginVersion { +// GetAllPluginPackages gets all PluginPackages which may be available. +func GetAllPluginPackages() PluginPackages { + if allPluginPackages == nil { + getOption := func(name string) []string { + data := GetOption(name) + if strs, ok := data.([]string); ok { + return strs + } + if ifs, ok := data.([]interface{}); ok { + result := make([]string, len(ifs)) + for i, urlIf := range ifs { + if url, ok := urlIf.(string); ok { + result[i] = url + } else { + return nil + } + } + return result + } + return nil + } + + channels := PluginChannels{} + for _, url := range getOption("pluginchannels") { + channels = append(channels, PluginChannel(url)) + } + repos := []PluginRepository{} + for _, url := range getOption("pluginrepos") { + repos = append(repos, PluginRepository(url)) + } + allPluginPackages = fetchAllSources(len(repos)+1, func(i int) PluginPackages { + if i == 0 { + return channels.Fetch() + } + return repos[i-1].Fetch() + }) + } + return allPluginPackages +} + +func (pv PluginVersions) find(ppName string) *PluginVersion { for _, v := range pv { - if v.pack.Name == name { + if v.pack.Name == ppName { return v } } return nil } + +// Len returns the number of pluginversions in this slice func (pv PluginVersions) Len() int { return len(pv) } +// Swap two entries of the slice func (pv PluginVersions) Swap(i, j int) { pv[i], pv[j] = pv[j], pv[i] } -func (s PluginVersions) Less(i, j int) bool { - // sort descending - return s[i].Version.GT(s[j].Version) +// Less returns true if the version at position i is greater then the version at position j (used for sorting) +func (pv PluginVersions) Less(i, j int) bool { + return pv[i].Version.GT(pv[j].Version) } -func (pr PluginRepository) Query() <-chan *PluginPackage { - resChan := make(chan *PluginPackage) - go func() { - defer close(resChan) - - resp, err := http.Get(string(pr)) - if err != nil { - TermMessage("Failed to query plugin repository:\n", err) - return +// Match returns true if the package matches a given search text +func (pp PluginPackage) Match(text string) bool { + text = strings.ToLower(text) + for _, t := range pp.Tags { + if strings.ToLower(t) == text { + return true } - defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) + } + if strings.Contains(strings.ToLower(pp.Name), text) { + return true + } - var plugins PluginPackages - if err := decoder.Decode(&plugins); err != nil { - TermMessage("Failed to decode repository data:\n", err) - return - } - for _, p := range plugins { - resChan <- p - } - }() - return resChan + if strings.Contains(strings.ToLower(pp.Description), text) { + return true + } + + return false +} + +// IsInstallable returns true if the package can be installed. +func (pp PluginPackage) IsInstallable() error { + _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ + &PluginDependency{ + Name: pp.Name, + Range: semver.Range(func(v semver.Version) bool { return true }), + }}) + return err } -func (pp *PluginPackage) GetInstallableVersion() *PluginVersion { - matching := make(PluginVersions, 0) +// SearchPlugin retrieves a list of all PluginPackages which match the given search text and +// could be or are already installed +func SearchPlugin(texts []string) (plugins PluginPackages) { + plugins = make(PluginPackages, 0) -versionLoop: - for _, pv := range pp.Versions { - for _, req := range pv.Require { - curVersion := GetInstalledVersion(req.Name) - if curVersion == nil || !req.Range(*curVersion) { - continue versionLoop +pluginLoop: + for _, pp := range GetAllPluginPackages() { + for _, text := range texts { + if !pp.Match(text) { + continue pluginLoop } } - matching = append(matching, pv) - } - if len(matching) > 0 { - sort.Sort(matching) - return matching[0] + + if err := pp.IsInstallable(); err == nil { + plugins = append(plugins, pp) + } } - return nil + return } -func (pp PluginPackage) Match(text string) bool { - // ToDo: improve matching. - text = "(?i)" + text - if r, err := regexp.Compile(text); err == nil { - return r.MatchString(pp.Name) - } - return false +func isUnknownCoreVersion() bool { + _, err := semver.ParseTolerant(Version) + return err != nil } -func SearchPlugin(text string) (plugins []*PluginPackage) { - wgQuery := new(sync.WaitGroup) - wgQuery.Add(len(Repositories)) - results := make(chan *PluginPackage) +func newStaticPluginVersion(name, version string) *PluginVersion { + vers, err := semver.ParseTolerant(version) - wgDone := new(sync.WaitGroup) - wgDone.Add(1) - for _, repo := range Repositories { - go func(repo PluginRepository) { - res := repo.Query() - for r := range res { - results <- r - } - wgQuery.Done() - }(repo) - } - go func() { - for res := range results { - if res.GetInstallableVersion() != nil && res.Match(text) { - plugins = append(plugins, res) - } + if err != nil { + if vers, err = semver.ParseTolerant("0.0.0-" + version); err != nil { + vers = semver.MustParse("0.0.0-unknown") } - wgDone.Done() - }() - wgQuery.Wait() - close(results) - wgDone.Wait() - return + } + pl := &PluginPackage{ + Name: name, + } + pv := &PluginVersion{ + pack: pl, + Version: vers, + } + pl.Versions = PluginVersions{pv} + return pv } -func GetInstalledVersion(name string) *semver.Version { - versionStr := "" - if name == "micro" { - versionStr = Version +// GetInstalledVersions returns a list of all currently installed plugins including an entry for +// micro itself. This can be used to resolve dependencies. +func GetInstalledVersions(withCore bool) PluginVersions { + result := PluginVersions{} + if withCore { + result = append(result, newStaticPluginVersion(CorePluginName, Version)) + } - } else { - plugin := L.GetGlobal(name) - if plugin == lua.LNil { - return nil + for name, lpname := range loadedPlugins { + version := GetInstalledPluginVersion(lpname) + if pv := newStaticPluginVersion(name, version); pv != nil { + result = append(result, pv) } + } + + return result +} + +// GetInstalledPluginVersion returns the string of the exported VERSION variable of a loaded plugin +func GetInstalledPluginVersion(name string) string { + plugin := L.GetGlobal(name) + if plugin != lua.LNil { version := L.GetField(plugin, "VERSION") if str, ok := version.(lua.LString); ok { - versionStr = string(str) - } - } + return string(str) - if v, err := semver.Parse(versionStr); err != nil { - return nil - } else { - return &v + } } + return "" } -func (pv *PluginVersion) Install() { +// DownloadAndInstall downloads and installs the given plugin and version +func (pv *PluginVersion) DownloadAndInstall() error { + messenger.AddLog(fmt.Sprintf("Downloading %q (%s) from %q", pv.pack.Name, pv.Version, pv.Url)) resp, err := http.Get(pv.Url) - if err == nil { - defer resp.Body.Close() - data, _ := ioutil.ReadAll(resp.Body) - zipbuf := bytes.NewReader(data) - z, err := zip.NewReader(zipbuf, zipbuf.Size()) - if err == nil { - targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) - dirPerm := os.FileMode(0755) - if err = os.MkdirAll(targetDir, dirPerm); err == nil { - for _, f := range z.File { - targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) - if f.FileInfo().IsDir() { - err = os.MkdirAll(targetName, dirPerm) - } else { - content, err := f.Open() - if err == nil { - defer content.Close() - if target, err := os.Create(targetName); err == nil { - defer target.Close() - _, err = io.Copy(target, content) - } - } - } - if err != nil { - break - } - } - } - } + if err != nil { + return err } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) if err != nil { - TermMessage("Failed to install plugin:", err) + return err + } + zipbuf := bytes.NewReader(data) + z, err := zip.NewReader(zipbuf, zipbuf.Size()) + if err != nil { + return err + } + targetDir := filepath.Join(configDir, "plugins", pv.pack.Name) + dirPerm := os.FileMode(0755) + if err = os.MkdirAll(targetDir, dirPerm); err != nil { + return err } -} -func UninstallPlugin(name string) { - os.RemoveAll(filepath.Join(configDir, name)) -} + // Check if all files in zip are in the same directory. + // this might be the case if the plugin zip contains the whole plugin dir + // instead of its content. + var prefix string + allPrefixed := false + for i, f := range z.File { + parts := strings.Split(f.Name, "/") + if i == 0 { + prefix = parts[0] + } else if parts[0] != prefix { + allPrefixed = false + break + } else { + // switch to true since we have at least a second file + allPrefixed = true + } + } -// Updates... + for _, f := range z.File { + parts := strings.Split(f.Name, "/") + if allPrefixed { + parts = parts[1:] + } + + targetName := filepath.Join(targetDir, filepath.Join(parts...)) + if f.FileInfo().IsDir() { + if err := os.MkdirAll(targetName, dirPerm); err != nil { + return err + } + } else { + content, err := f.Open() + if err != nil { + return err + } + defer content.Close() + target, err := os.Create(targetName) + if err != nil { + return err + } + defer target.Close() + if _, err = io.Copy(target, content); err != nil { + return err + } + } + } + return nil +} func (pl PluginPackages) Get(name string) *PluginPackage { for _, p := range pl { @@ -313,33 +495,121 @@ func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies return result } -func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { +// Resolve resolves dependencies between different plugins +func (all PluginPackages) Resolve(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { if len(open) == 0 { return selectedVersions, nil } currentRequirement, stillOpen := open[0], open[1:] if currentRequirement != nil { - if selVersion := selectedVersions.Find(currentRequirement.Name); selVersion != nil { + if selVersion := selectedVersions.find(currentRequirement.Name); selVersion != nil { if currentRequirement.Range(selVersion.Version) { - return all.ResolveStep(selectedVersions, stillOpen) + return all.Resolve(selectedVersions, stillOpen) } return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) - } else { - availableVersions := all.GetAllVersions(currentRequirement.Name) - sort.Sort(availableVersions) + } + availableVersions := all.GetAllVersions(currentRequirement.Name) + sort.Sort(availableVersions) - for _, version := range availableVersions { - if currentRequirement.Range(version.Version) { - resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require)) + for _, version := range availableVersions { + if currentRequirement.Range(version.Version) { + resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require)) - if err == nil { - return resolved, nil - } + if err == nil { + return resolved, nil } } - return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) } + return nil, fmt.Errorf("unable to find a matching version for \"%s\"", currentRequirement.Name) + } + return selectedVersions, nil +} + +func (pv PluginVersions) install() { + anyInstalled := false + currentlyInstalled := GetInstalledVersions(true) + + for _, sel := range pv { + if sel.pack.Name != CorePluginName { + shouldInstall := true + if pv := currentlyInstalled.find(sel.pack.Name); pv != nil { + if pv.Version.NE(sel.Version) { + messenger.AddLog(fmt.Sprint("Uninstalling %q", sel.pack.Name)) + UninstallPlugin(sel.pack.Name) + } else { + shouldInstall = false + } + } + + if shouldInstall { + if err := sel.DownloadAndInstall(); err != nil { + messenger.Error(err) + return + } + anyInstalled = true + } + } + } + if anyInstalled { + messenger.Message("One or more plugins installed. Please restart micro.") } else { - return selectedVersions, nil + messenger.AddLog("Nothing to install / update") + } +} + +// UninstallPlugin deletes the plugin folder of the given plugin +func UninstallPlugin(name string) { + if err := os.RemoveAll(filepath.Join(configDir, "plugins", name)); err != nil { + messenger.Error(err) + return + } + delete(loadedPlugins, name) +} + +// Install installs the plugin +func (pl PluginPackage) Install() { + selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(true), PluginDependencies{ + &PluginDependency{ + Name: pl.Name, + Range: semver.Range(func(v semver.Version) bool { return true }), + }}) + if err != nil { + TermMessage(err) + return + } + selected.install() +} + +// UpdatePlugins updates the given plugins +func UpdatePlugins(plugins []string) { + // if no plugins are specified, update all installed plugins. + if len(plugins) == 0 { + for name := range loadedPlugins { + plugins = append(plugins, name) + } + } + + messenger.AddLog("Checking for plugin updates") + microVersion := PluginVersions{ + newStaticPluginVersion(CorePluginName, Version), + } + + var updates = make(PluginDependencies, 0) + for _, name := range plugins { + pv := GetInstalledPluginVersion(name) + r, err := semver.ParseRange(">=" + pv) // Try to get newer versions. + if err == nil { + updates = append(updates, &PluginDependency{ + Name: name, + Range: r, + }) + } + } + + selected, err := GetAllPluginPackages().Resolve(microVersion, updates) + if err != nil { + TermMessage(err) + return } + selected.install() }