From f351c251e46253c334ad641ce6d083dc8565ea5f Mon Sep 17 00:00:00 2001 From: Florian Sundermann Date: Mon, 26 Sep 2016 16:53:39 +0200 Subject: [PATCH] first few pm commands --- cmd/micro/command.go | 35 ++++ cmd/micro/micro.go | 2 +- cmd/micro/pluginmanager.go | 419 ++++++++++++++++++++++++------------- 3 files changed, 314 insertions(+), 142 deletions(-) diff --git a/cmd/micro/command.go b/cmd/micro/command.go index f0cfe9b3..60b64558 100644 --- a/cmd/micro/command.go +++ b/cmd/micro/command.go @@ -39,6 +39,7 @@ var commandActions = map[string]func([]string){ "Tab": NewTab, "Help": Help, "Eval": Eval, + "Plugin": PluginCmd, } // InitCommands initializes the default commands @@ -84,6 +85,40 @@ func DefaultCommands() map[string]StrCommand { "tab": {"Tab", []Completion{FileCompletion, NoCompletion}}, "help": {"Help", []Completion{HelpCompletion, NoCompletion}}, "eval": {"Eval", []Completion{NoCompletion}}, + "plugin": {"Plugin", []Completion{NoCompletion}}, + } +} + +// InstallPlugin installs the given plugin by exact name match +func PluginCmd(args []string) { + if len(args) >= 1 { + switch args[0] { + case "install": + for _, plugin := range args[1:] { + pp := GetAllPluginPackages().Get(plugin) + if pp == nil { + messenger.Error("Unknown plugin \"" + plugin + "\"") + } else if !pp.IsInstallable() { + messenger.Error("Plugin \"" + plugin + "\" can not be installed.") + } else { + pp.Install() + } + } + case "remove": + for _, plugin := range args[1:] { + // check if the plugin exists. + for _, lp := range loadedPlugins { + if lp == plugin { + UninstallPlugin(plugin) + continue + } + } + } + case "update": + UpdatePlugins() + } + } else { + messenger.Error("Not enough arguments") } } diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index 2d2829c8..a3a75b23 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -44,7 +44,7 @@ var ( // Version is the version number or commit hash // These variables should be set by the linker when compiling - Version = "Unknown" + Version = "2.0.0" CommitHash = "Unknown" CompileDate = "Unknown" diff --git a/cmd/micro/pluginmanager.go b/cmd/micro/pluginmanager.go index 25e0890d..43296473 100644 --- a/cmd/micro/pluginmanager.go +++ b/cmd/micro/pluginmanager.go @@ -19,10 +19,24 @@ import ( "github.com/yuin/gopher-lua" ) -var Repositories []PluginRepository = []PluginRepository{} +var ( + pluginChannels PluginChannels = PluginChannels{ + PluginChannel("https://www.boombuler.de/channel.json"), + } + allPluginPackages PluginPackages = nil +) + +// 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 +45,103 @@ 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 +// PluginDenendency 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 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 { + resp, err := http.Get(string(pc)) + if err != nil { + TermMessage("Failed to query plugin channel:\n", err) + return PluginPackages{} + } + defer resp.Body.Close() + decoder := json.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 { + resp, err := http.Get(string(pr)) + if err != nil { + TermMessage("Failed to query plugin repository:\n", err) + return PluginPackages{} + } + defer resp.Body.Close() + decoder := json.NewDecoder(resp.Body) + + var plugins PluginPackages + if err := decoder.Decode(&plugins); err != nil { + TermMessage("Failed to decode repository data:\n", err) + return PluginPackages{} + } + return plugins +} + +// UnmarshalJSON unmarshals raw json to a PluginVersion func (pv *PluginVersion) UnmarshalJSON(data []byte) error { var values struct { Version semver.Version @@ -69,14 +164,7 @@ func (pv *PluginVersion) UnmarshalJSON(data []byte) error { 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 @@ -99,72 +187,39 @@ 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 { + allPluginPackages = pluginChannels.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] } +// Less returns true if the version at position i is greater then the version at position j (used for sorting) func (s PluginVersions) Less(i, j int) bool { - // sort descending return s[i].Version.GT(s[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 - } - defer resp.Body.Close() - decoder := json.NewDecoder(resp.Body) - - 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 -} - -func (pp *PluginPackage) GetInstallableVersion() *PluginVersion { - matching := make(PluginVersions, 0) - -versionLoop: - for _, pv := range pp.Versions { - for _, req := range pv.Require { - curVersion := GetInstalledVersion(req.Name) - if curVersion == nil || !req.Range(*curVersion) { - continue versionLoop - } - } - matching = append(matching, pv) - } - if len(matching) > 0 { - sort.Sort(matching) - return matching[0] - } - return nil -} - +// Match returns true if the package matches a given search text func (pp PluginPackage) Match(text string) bool { // ToDo: improve matching. text = "(?i)" + text @@ -174,102 +229,119 @@ func (pp PluginPackage) Match(text string) bool { return false } -func SearchPlugin(text string) (plugins []*PluginPackage) { - wgQuery := new(sync.WaitGroup) - wgQuery.Add(len(Repositories)) - results := make(chan *PluginPackage) +// IsInstallable returns true if the package can be installed. +func (pp PluginPackage) IsInstallable() bool { + _, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + &PluginDependency{ + Name: pp.Name, + Range: semver.Range(func(v semver.Version) bool { return true }), + }}) + return err == nil +} - 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) - } +// SearchPlugin retrieves a list of all PluginPackages which match the given search text and +// could be or are already installed +func SearchPlugin(text string) (plugins PluginPackages) { + plugins = make(PluginPackages, 0) + for _, pp := range GetAllPluginPackages() { + if pp.Match(text) && pp.IsInstallable() { + plugins = append(plugins, pp) } - wgDone.Done() - }() - wgQuery.Wait() - close(results) - wgDone.Wait() + } return } -func GetInstalledVersion(name string) *semver.Version { - versionStr := "" - if name == "micro" { - versionStr = Version - - } else { - plugin := L.GetGlobal(name) - if plugin == lua.LNil { - return nil - } - version := L.GetField(plugin, "VERSION") - if str, ok := version.(lua.LString); ok { - versionStr = string(str) - } - } - - if v, err := semver.Parse(versionStr); err != nil { +func newStaticPluginVersion(name, version string) *PluginVersion { + vers, err := semver.Parse(version) + if err != nil { return nil - } else { - return &v } + pl := &PluginPackage{ + Name: name, + } + pv := &PluginVersion{ + pack: pl, + Version: vers, + } + pl.Versions = PluginVersions{pv} + return pv } -func (pv *PluginVersion) Install() { +// GetInstalledVersions returns a list of all currently installed plugins including an entry for +// micro itself. This can be used to resolve dependencies. +func GetInstalledVersions() PluginVersions { + result := PluginVersions{ + newStaticPluginVersion("micro", Version), + } + + for _, name := range loadedPlugins { + version := GetInstalledPluginVersion(name) + 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 { + return string(str) + + } + } + return "" +} + +func (pv *PluginVersion) DownloadAndInstall() error { 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 { + 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 + } + for _, f := range z.File { + targetName := filepath.Join(targetDir, filepath.Join(strings.Split(f.Name, "/")...)) + 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() + if target, err := os.Create(targetName); err != nil { + return err + } else { + defer target.Close() + if _, err = io.Copy(target, content); err != nil { + return err } } } } - if err != nil { - TermMessage("Failed to install plugin:", err) - } + return nil } -func UninstallPlugin(name string) { - os.RemoveAll(filepath.Join(configDir, name)) -} - -// Updates... - func (pl PluginPackages) Get(name string) *PluginPackage { for _, p := range pl { if p.Name == name { @@ -313,15 +385,15 @@ func (req PluginDependencies) Join(other PluginDependencies) PluginDependencies return result } -func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open PluginDependencies) (PluginVersions, error) { +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 { @@ -330,7 +402,7 @@ func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open Plug for _, version := range availableVersions { if currentRequirement.Range(version.Version) { - resolved, err := all.ResolveStep(append(selectedVersions, version), stillOpen.Join(version.Require)) + resolved, err := all.Resolve(append(selectedVersions, version), stillOpen.Join(version.Require)) if err == nil { return resolved, nil @@ -343,3 +415,68 @@ func (all PluginPackages) ResolveStep(selectedVersions PluginVersions, open Plug return selectedVersions, nil } } + +func (versions PluginVersions) install() { + anyInstalled := false + for _, sel := range versions { + if sel.pack.Name != "micro" { + installed := GetInstalledPluginVersion(sel.pack.Name) + if v, err := semver.Parse(installed); err != nil || v.NE(sel.Version) { + UninstallPlugin(sel.pack.Name) + } + if err := sel.DownloadAndInstall(); err != nil { + messenger.Error(err) + return + } + anyInstalled = true + } + } + if anyInstalled { + messenger.Message("One or more plugins installed. Please restart micro.") + } +} + +// 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) + } +} + +func (pl PluginPackage) Install() { + selected, err := GetAllPluginPackages().Resolve(GetInstalledVersions(), PluginDependencies{ + &PluginDependency{ + Name: pl.Name, + Range: semver.Range(func(v semver.Version) bool { return true }), + }}) + if err != nil { + TermMessage(err) + return + } + selected.install() +} + +func UpdatePlugins() { + microVersion := PluginVersions{ + newStaticPluginVersion("micro", Version), + } + + var updates = make(PluginDependencies, 0) + for _, name := range loadedPlugins { + 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() +}