github.com/mattdotmatt/gauge@v0.3.2-0.20160421115137-425a4cdccb62/plugin/plugin.go (about)

     1  // Copyright 2015 ThoughtWorks, Inc.
     2  
     3  // This file is part of Gauge.
     4  
     5  // Gauge is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  
    10  // Gauge is distributed in the hope that it will be useful,
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU General Public License for more details.
    14  
    15  // You should have received a copy of the GNU General Public License
    16  // along with Gauge.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package plugin
    19  
    20  import (
    21  	"encoding/json"
    22  	"fmt"
    23  	"io/ioutil"
    24  	"net"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"runtime"
    29  	"sort"
    30  	"strconv"
    31  	"strings"
    32  	"sync"
    33  	"time"
    34  
    35  	"github.com/getgauge/common"
    36  	"github.com/getgauge/gauge/config"
    37  	"github.com/getgauge/gauge/conn"
    38  	"github.com/getgauge/gauge/gauge_messages"
    39  	"github.com/getgauge/gauge/logger"
    40  	"github.com/getgauge/gauge/manifest"
    41  	"github.com/getgauge/gauge/reporter"
    42  	"github.com/getgauge/gauge/version"
    43  	"github.com/golang/protobuf/proto"
    44  )
    45  
    46  const (
    47  	executionScope          = "execution"
    48  	pluginConnectionPortEnv = "plugin_connection_port"
    49  )
    50  
    51  type pluginDescriptor struct {
    52  	ID          string
    53  	Version     string
    54  	Name        string
    55  	Description string
    56  	Command     struct {
    57  		Windows []string
    58  		Linux   []string
    59  		Darwin  []string
    60  	}
    61  	Scope               []string
    62  	GaugeVersionSupport version.VersionSupport
    63  	pluginPath          string
    64  }
    65  
    66  type Handler struct {
    67  	pluginsMap map[string]*plugin
    68  }
    69  
    70  type plugin struct {
    71  	mutex      *sync.Mutex
    72  	connection net.Conn
    73  	pluginCmd  *exec.Cmd
    74  	descriptor *pluginDescriptor
    75  }
    76  
    77  func (p *plugin) IsProcessRunning() bool {
    78  	p.mutex.Lock()
    79  	ps := p.pluginCmd.ProcessState
    80  	p.mutex.Unlock()
    81  	return ps == nil || !ps.Exited()
    82  }
    83  
    84  func (p *plugin) kill(wg *sync.WaitGroup) error {
    85  	defer wg.Done()
    86  	if p.IsProcessRunning() {
    87  		defer p.connection.Close()
    88  		conn.SendProcessKillMessage(p.connection)
    89  
    90  		exited := make(chan bool, 1)
    91  		go func() {
    92  			for {
    93  				if p.IsProcessRunning() {
    94  					time.Sleep(100 * time.Millisecond)
    95  				} else {
    96  					exited <- true
    97  					return
    98  				}
    99  			}
   100  		}()
   101  		select {
   102  		case done := <-exited:
   103  			if done {
   104  				logger.Debug("Plugin [%s] with pid [%d] has exited", p.descriptor.Name, p.pluginCmd.Process.Pid)
   105  			}
   106  		case <-time.After(config.PluginConnectionTimeout()):
   107  			logger.Warning("Plugin [%s] with pid [%d] did not exit after %.2f seconds. Forcefully killing it.", p.descriptor.Name, p.pluginCmd.Process.Pid, config.PluginConnectionTimeout().Seconds())
   108  			err := p.pluginCmd.Process.Kill()
   109  			if err != nil {
   110  				logger.Warning("Error while killing plugin %s : %s ", p.descriptor.Name, err.Error())
   111  			}
   112  			return err
   113  		}
   114  	}
   115  	return nil
   116  }
   117  
   118  func IsPluginInstalled(pluginName, pluginVersion string) bool {
   119  	pluginsInstallDir, err := common.GetPluginsInstallDir(pluginName)
   120  	if err != nil {
   121  		return false
   122  	}
   123  
   124  	thisPluginDir := filepath.Join(pluginsInstallDir, pluginName)
   125  	if !common.DirExists(thisPluginDir) {
   126  		return false
   127  	}
   128  
   129  	if pluginVersion != "" {
   130  		pluginJSON := filepath.Join(thisPluginDir, pluginVersion, common.PluginJSONFile)
   131  		if common.FileExists(pluginJSON) {
   132  			return true
   133  		}
   134  		return false
   135  	}
   136  	return true
   137  }
   138  
   139  func getPluginJSONPath(pluginName, pluginVersion string) (string, error) {
   140  	if !IsPluginInstalled(pluginName, pluginVersion) {
   141  		return "", fmt.Errorf("Plugin %s %s is not installed", pluginName, pluginVersion)
   142  	}
   143  
   144  	pluginInstallDir, err := GetInstallDir(pluginName, "")
   145  	if err != nil {
   146  		return "", err
   147  	}
   148  	return filepath.Join(pluginInstallDir, common.PluginJSONFile), nil
   149  }
   150  
   151  func GetPluginDescriptor(pluginID, pluginVersion string) (*pluginDescriptor, error) {
   152  	pluginJSON, err := getPluginJSONPath(pluginID, pluginVersion)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  	return GetPluginDescriptorFromJSON(pluginJSON)
   157  }
   158  
   159  func GetPluginDescriptorFromJSON(pluginJSON string) (*pluginDescriptor, error) {
   160  	pluginJSONContents, err := common.ReadFileContents(pluginJSON)
   161  	if err != nil {
   162  		return nil, err
   163  	}
   164  	var pd pluginDescriptor
   165  	if err = json.Unmarshal([]byte(pluginJSONContents), &pd); err != nil {
   166  		return nil, fmt.Errorf("%s: %s", pluginJSON, err.Error())
   167  	}
   168  	pd.pluginPath = filepath.Dir(pluginJSON)
   169  
   170  	return &pd, nil
   171  }
   172  
   173  func StartPlugin(pd *pluginDescriptor, action string) (*plugin, error) {
   174  	command := []string{}
   175  	switch runtime.GOOS {
   176  	case "windows":
   177  		command = pd.Command.Windows
   178  		break
   179  	case "darwin":
   180  		command = pd.Command.Darwin
   181  		break
   182  	default:
   183  		command = pd.Command.Linux
   184  		break
   185  	}
   186  	if len(command) == 0 {
   187  		return nil, fmt.Errorf("Platform specific command not specified: %s.", runtime.GOOS)
   188  	}
   189  
   190  	cmd, err := common.ExecuteCommand(command, pd.pluginPath, reporter.Current(), reporter.Current())
   191  
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  	var mutex = &sync.Mutex{}
   196  	go func() {
   197  		pState, _ := cmd.Process.Wait()
   198  		mutex.Lock()
   199  		cmd.ProcessState = pState
   200  		mutex.Unlock()
   201  	}()
   202  	plugin := &plugin{pluginCmd: cmd, descriptor: pd, mutex: mutex}
   203  	return plugin, nil
   204  }
   205  
   206  func SetEnvForPlugin(action string, pd *pluginDescriptor, manifest *manifest.Manifest, pluginEnvVars map[string]string) error {
   207  	pluginEnvVars[fmt.Sprintf("%s_action", pd.ID)] = action
   208  	pluginEnvVars["test_language"] = manifest.Language
   209  	if err := setEnvironmentProperties(pluginEnvVars); err != nil {
   210  		return err
   211  	}
   212  	return nil
   213  }
   214  
   215  func setEnvironmentProperties(properties map[string]string) error {
   216  	for k, v := range properties {
   217  		if err := common.SetEnvVariable(k, v); err != nil {
   218  			return err
   219  		}
   220  	}
   221  	return nil
   222  }
   223  
   224  func IsPluginAdded(manifest *manifest.Manifest, descriptor *pluginDescriptor) bool {
   225  	for _, pluginID := range manifest.Plugins {
   226  		if pluginID == descriptor.ID {
   227  			return true
   228  		}
   229  	}
   230  	return false
   231  }
   232  
   233  func startPluginsForExecution(manifest *manifest.Manifest) (*Handler, []string) {
   234  	var warnings []string
   235  	handler := &Handler{}
   236  	envProperties := make(map[string]string)
   237  
   238  	for _, pluginID := range manifest.Plugins {
   239  		pd, err := GetPluginDescriptor(pluginID, "")
   240  		if err != nil {
   241  			warnings = append(warnings, fmt.Sprintf("Error starting plugin %s. Failed to get plugin.json. %s. To install, run `gauge --install %s`.", pluginID, err.Error(), pluginID))
   242  			continue
   243  		}
   244  		compatibilityErr := version.CheckCompatibility(version.CurrentGaugeVersion, &pd.GaugeVersionSupport)
   245  		if compatibilityErr != nil {
   246  			warnings = append(warnings, fmt.Sprintf("Compatible %s plugin version to current Gauge version %s not found", pd.Name, version.CurrentGaugeVersion))
   247  			continue
   248  		}
   249  		if isExecutionScopePlugin(pd) {
   250  			gaugeConnectionHandler, err := conn.NewGaugeConnectionHandler(0, nil)
   251  			if err != nil {
   252  				warnings = append(warnings, err.Error())
   253  				continue
   254  			}
   255  			envProperties[pluginConnectionPortEnv] = strconv.Itoa(gaugeConnectionHandler.ConnectionPortNumber())
   256  			err = SetEnvForPlugin(executionScope, pd, manifest, envProperties)
   257  			if err != nil {
   258  				warnings = append(warnings, fmt.Sprintf("Error setting environment for plugin %s %s. %s", pd.Name, pd.Version, err.Error()))
   259  				continue
   260  			}
   261  
   262  			plugin, err := StartPlugin(pd, executionScope)
   263  			if err != nil {
   264  				warnings = append(warnings, fmt.Sprintf("Error starting plugin %s %s. %s", pd.Name, pd.Version, err.Error()))
   265  				continue
   266  			}
   267  			pluginConnection, err := gaugeConnectionHandler.AcceptConnection(config.PluginConnectionTimeout(), make(chan error))
   268  			if err != nil {
   269  				warnings = append(warnings, fmt.Sprintf("Error starting plugin %s %s. Failed to connect to plugin. %s", pd.Name, pd.Version, err.Error()))
   270  				plugin.pluginCmd.Process.Kill()
   271  				continue
   272  			}
   273  			plugin.connection = pluginConnection
   274  			handler.addPlugin(pluginID, plugin)
   275  		}
   276  
   277  	}
   278  	return handler, warnings
   279  }
   280  
   281  func isExecutionScopePlugin(pd *pluginDescriptor) bool {
   282  	for _, scope := range pd.Scope {
   283  		if strings.ToLower(scope) == executionScope {
   284  			return true
   285  		}
   286  	}
   287  	return false
   288  }
   289  
   290  func (handler *Handler) addPlugin(pluginID string, pluginToAdd *plugin) {
   291  	if handler.pluginsMap == nil {
   292  		handler.pluginsMap = make(map[string]*plugin)
   293  	}
   294  	handler.pluginsMap[pluginID] = pluginToAdd
   295  }
   296  
   297  func (handler *Handler) removePlugin(pluginID string) {
   298  	delete(handler.pluginsMap, pluginID)
   299  }
   300  
   301  func (handler *Handler) NotifyPlugins(message *gauge_messages.Message) {
   302  	for id, plugin := range handler.pluginsMap {
   303  		err := plugin.sendMessage(message)
   304  		if err != nil {
   305  			logger.Errorf("Unable to connect to plugin %s %s. %s\n", plugin.descriptor.Name, plugin.descriptor.Version, err.Error())
   306  			handler.killPlugin(id)
   307  		}
   308  	}
   309  }
   310  
   311  func (handler *Handler) killPlugin(pluginID string) {
   312  	plugin := handler.pluginsMap[pluginID]
   313  	logger.Debug("Killing Plugin %s %s\n", plugin.descriptor.Name, plugin.descriptor.Version)
   314  	err := plugin.pluginCmd.Process.Kill()
   315  	if err != nil {
   316  		logger.Errorf("Failed to kill plugin %s %s. %s\n", plugin.descriptor.Name, plugin.descriptor.Version, err.Error())
   317  	}
   318  	handler.removePlugin(pluginID)
   319  }
   320  
   321  func (handler *Handler) GracefullyKillPlugins() {
   322  	var wg sync.WaitGroup
   323  	for _, plugin := range handler.pluginsMap {
   324  		wg.Add(1)
   325  		go plugin.kill(&wg)
   326  	}
   327  	wg.Wait()
   328  }
   329  
   330  func (p *plugin) sendMessage(message *gauge_messages.Message) error {
   331  	messageID := common.GetUniqueID()
   332  	message.MessageId = &messageID
   333  	messageBytes, err := proto.Marshal(message)
   334  	if err != nil {
   335  		return err
   336  	}
   337  	err = conn.Write(p.connection, messageBytes)
   338  	if err != nil {
   339  		return fmt.Errorf("[Warning] Failed to send message to plugin: %s  %s", p.descriptor.ID, err.Error())
   340  	}
   341  	return nil
   342  }
   343  
   344  func StartPlugins(manifest *manifest.Manifest) *Handler {
   345  	pluginHandler, warnings := startPluginsForExecution(manifest)
   346  	logger.HandleWarningMessages(warnings)
   347  	return pluginHandler
   348  }
   349  
   350  func getLatestInstalledPlugin(pluginDir string) (*PluginInfo, error) {
   351  	files, err := ioutil.ReadDir(pluginDir)
   352  	if err != nil {
   353  		return nil, fmt.Errorf("Error listing files in plugin directory %s: %s", pluginDir, err.Error())
   354  	}
   355  	versionToPlugins := make(map[*version.Version][]PluginInfo, 0)
   356  	pluginName := filepath.Base(pluginDir)
   357  
   358  	for _, file := range files {
   359  		if file.IsDir() {
   360  			var v *version.Version
   361  			var err error
   362  			if strings.Contains(file.Name(), "nightly") {
   363  				v, err = version.ParseVersion(file.Name()[:strings.LastIndex(file.Name(), ".")])
   364  			} else {
   365  				v, err = version.ParseVersion(file.Name())
   366  			}
   367  			if err == nil {
   368  				versionToPlugins[v] = append(versionToPlugins[v], PluginInfo{pluginName, v, filepath.Join(pluginDir, file.Name())})
   369  			}
   370  		}
   371  	}
   372  
   373  	if len(versionToPlugins) < 1 {
   374  		return nil, fmt.Errorf("No valid versions of plugin %s found in %s", pluginName, pluginDir)
   375  	}
   376  	var availableVersions []*version.Version
   377  	for k := range versionToPlugins {
   378  		availableVersions = append(availableVersions, k)
   379  	}
   380  	latestVersion := version.GetLatestVersion(availableVersions)
   381  	latestBuild := getLatestOf(versionToPlugins[latestVersion], latestVersion)
   382  	return &latestBuild, nil
   383  }
   384  
   385  func getLatestOf(plugins []PluginInfo, latestVersion *version.Version) PluginInfo {
   386  	for _, v := range plugins {
   387  		if v.Path == latestVersion.String() {
   388  			return v
   389  		}
   390  	}
   391  	sort.Sort(byPath(plugins))
   392  	return plugins[0]
   393  }
   394  
   395  func GetAllInstalledPluginsWithVersion() ([]PluginInfo, error) {
   396  	pluginInstallPrefixes, err := common.GetPluginInstallPrefixes()
   397  	if err != nil {
   398  		return nil, err
   399  	}
   400  	allPlugins := make(map[string]PluginInfo, 0)
   401  	for _, prefix := range pluginInstallPrefixes {
   402  		files, err := ioutil.ReadDir(prefix)
   403  		if err != nil {
   404  			return nil, err
   405  		}
   406  		for _, file := range files {
   407  			pluginDir, err := os.Stat(filepath.Join(prefix, file.Name()))
   408  			if err != nil {
   409  				continue
   410  			}
   411  			if pluginDir.IsDir() {
   412  				latestPlugin, err := getLatestInstalledPlugin(filepath.Join(prefix, file.Name()))
   413  				if err != nil {
   414  					continue
   415  				}
   416  				pluginAdded, repeated := allPlugins[file.Name()]
   417  				if repeated {
   418  					var availableVersions []*version.Version
   419  					availableVersions = append(availableVersions, pluginAdded.Version, latestPlugin.Version)
   420  					latest := version.GetLatestVersion(availableVersions)
   421  					if latest.IsEqualTo(latestPlugin.Version) {
   422  						allPlugins[file.Name()] = *latestPlugin
   423  					}
   424  				} else {
   425  					allPlugins[file.Name()] = *latestPlugin
   426  				}
   427  			}
   428  		}
   429  	}
   430  	return sortPlugins(allPlugins), nil
   431  }
   432  
   433  type PluginInfo struct {
   434  	Name    string
   435  	Version *version.Version
   436  	Path    string
   437  }
   438  
   439  type byPluginName []PluginInfo
   440  
   441  func (a byPluginName) Len() int      { return len(a) }
   442  func (a byPluginName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   443  func (a byPluginName) Less(i, j int) bool {
   444  	return a[i].Name < a[j].Name
   445  }
   446  
   447  func sortPlugins(allPlugins map[string]PluginInfo) []PluginInfo {
   448  	var installedPlugins []PluginInfo
   449  	for _, plugin := range allPlugins {
   450  		installedPlugins = append(installedPlugins, plugin)
   451  	}
   452  	sort.Sort(byPluginName(installedPlugins))
   453  	return installedPlugins
   454  }
   455  
   456  type byPath []PluginInfo
   457  
   458  func (a byPath) Len() int      { return len(a) }
   459  func (a byPath) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
   460  func (a byPath) Less(i, j int) bool {
   461  	return a[i].Path > a[j].Path
   462  }
   463  
   464  func GetPluginsInfo() []PluginInfo {
   465  	allPluginsWithVersion, err := GetAllInstalledPluginsWithVersion()
   466  	if err != nil {
   467  		logger.Info("No plugins found")
   468  		logger.Info("Plugins can be installed with `gauge --install {plugin-name}`")
   469  		os.Exit(0)
   470  	}
   471  	return allPluginsWithVersion
   472  }
   473  
   474  // GetInstallDir returns the install directory of given plugin and a given version.
   475  func GetInstallDir(pluginName, version string) (string, error) {
   476  	allPluginsInstallDir, err := common.GetPluginsInstallDir(pluginName)
   477  	if err != nil {
   478  		return "", err
   479  	}
   480  	pluginDir := filepath.Join(allPluginsInstallDir, pluginName)
   481  	if version != "" {
   482  		pluginDir = filepath.Join(pluginDir, version)
   483  	} else {
   484  		latestPlugin, err := getLatestInstalledPlugin(pluginDir)
   485  		if err != nil {
   486  			return "", err
   487  		}
   488  		pluginDir = latestPlugin.Path
   489  	}
   490  	return pluginDir, nil
   491  }
   492  
   493  func GetLanguageJSONFilePath(language string) (string, error) {
   494  	languageInstallDir, err := GetInstallDir(language, "")
   495  	if err != nil {
   496  		return "", err
   497  	}
   498  	languageJSON := filepath.Join(languageInstallDir, fmt.Sprintf("%s.json", language))
   499  	if !common.FileExists(languageJSON) {
   500  		return "", fmt.Errorf("Failed to find the implementation for: %s. %s does not exist.", language, languageJSON)
   501  	}
   502  
   503  	return languageJSON, nil
   504  }
   505  
   506  func QueryParams() string {
   507  	return fmt.Sprintf("?l=%s&p=%s&o=%s&a=%s", language(), plugins(), runtime.GOOS, runtime.GOARCH)
   508  }
   509  
   510  func language() string {
   511  	if config.ProjectRoot == "" {
   512  		return ""
   513  	}
   514  	m, err := manifest.ProjectManifest()
   515  	if err != nil {
   516  		return ""
   517  	}
   518  	return m.Language
   519  }
   520  
   521  func plugins() string {
   522  	pluginInfos, err := GetAllInstalledPluginsWithVersion()
   523  	if err != nil {
   524  		return ""
   525  	}
   526  	var plugins []string
   527  	for _, p := range pluginInfos {
   528  		plugins = append(plugins, p.Name)
   529  	}
   530  	return strings.Join(plugins, ",")
   531  }