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 }