github.com/getgauge/gauge@v1.6.9/plugin/plugin.go (about) 1 /*---------------------------------------------------------------- 2 * Copyright (c) ThoughtWorks, Inc. 3 * Licensed under the Apache License, Version 2.0 4 * See LICENSE in the project root for license information. 5 *----------------------------------------------------------------*/ 6 7 package plugin 8 9 import ( 10 "context" 11 "encoding/json" 12 "fmt" 13 "net" 14 "os" 15 "os/exec" 16 "path/filepath" 17 "runtime" 18 "strconv" 19 "strings" 20 "sync" 21 "time" 22 23 "github.com/getgauge/common" 24 "github.com/getgauge/gauge-proto/go/gauge_messages" 25 "github.com/getgauge/gauge/api/infoGatherer" 26 "github.com/getgauge/gauge/config" 27 "github.com/getgauge/gauge/conn" 28 "github.com/getgauge/gauge/gauge" 29 "github.com/getgauge/gauge/logger" 30 "github.com/getgauge/gauge/manifest" 31 "github.com/getgauge/gauge/plugin/pluginInfo" 32 "github.com/getgauge/gauge/version" 33 "google.golang.org/grpc" 34 "google.golang.org/grpc/codes" 35 "google.golang.org/grpc/credentials/insecure" 36 "google.golang.org/grpc/status" 37 "google.golang.org/protobuf/proto" 38 ) 39 40 type pluginScope string 41 42 const ( 43 executionScope pluginScope = "execution" 44 docScope pluginScope = "documentation" 45 pluginConnectionPortEnv = "plugin_connection_port" 46 ) 47 48 type plugin struct { 49 mutex *sync.Mutex 50 connection net.Conn 51 gRPCConn *grpc.ClientConn 52 ReporterClient gauge_messages.ReporterClient 53 DocumenterClient gauge_messages.DocumenterClient 54 pluginCmd *exec.Cmd 55 descriptor *PluginDescriptor 56 killTimer *time.Timer 57 } 58 59 func isProcessRunning(p *plugin) bool { 60 p.mutex.Lock() 61 ps := p.pluginCmd.ProcessState 62 p.mutex.Unlock() 63 return ps == nil || !ps.Exited() 64 } 65 66 func (p *plugin) killGrpcProcess() error { 67 var m *gauge_messages.Empty 68 var err error 69 if p.ReporterClient != nil { 70 m, err = p.ReporterClient.Kill(context.Background(), &gauge_messages.KillProcessRequest{}) 71 } else if p.DocumenterClient != nil { 72 m, err = p.DocumenterClient.Kill(context.Background(), &gauge_messages.KillProcessRequest{}) 73 } 74 if m == nil || err != nil { 75 errStatus, _ := status.FromError(err) 76 if errStatus.Code() == codes.Unavailable { 77 // Ref https://www.grpc.io/docs/guides/error/#general-errors 78 // GRPC_STATUS_UNAVAILABLE is thrown when Server is shutting down. Ignore it here. 79 return nil 80 } 81 return err 82 } 83 if p.gRPCConn == nil && p.pluginCmd == nil { 84 return nil 85 } 86 defer p.gRPCConn.Close() 87 88 if isProcessRunning(p) { 89 exited := make(chan bool, 1) 90 go func() { 91 for { 92 if isProcessRunning(p) { 93 time.Sleep(100 * time.Millisecond) 94 } else { 95 exited <- true 96 return 97 } 98 } 99 }() 100 101 select { 102 case done := <-exited: 103 if done { 104 logger.Debugf(true, "Runner with PID:%d has exited", p.pluginCmd.Process.Pid) 105 return nil 106 } 107 case <-time.After(config.PluginKillTimeout()): 108 logger.Warningf(true, "Killing runner with PID:%d forcefully", p.pluginCmd.Process.Pid) 109 return p.pluginCmd.Process.Kill() 110 } 111 } 112 return nil 113 } 114 115 func (p *plugin) kill(wg *sync.WaitGroup) error { 116 defer wg.Done() 117 if p.gRPCConn != nil && p.ReporterClient != nil { 118 return p.killGrpcProcess() 119 } 120 if isProcessRunning(p) { 121 defer p.connection.Close() 122 p.killTimer = time.NewTimer(config.PluginKillTimeout()) 123 err := conn.SendProcessKillMessage(p.connection) 124 if err != nil { 125 logger.Warningf(true, "Error while killing plugin %s : %s ", p.descriptor.Name, err.Error()) 126 } 127 128 exited := make(chan bool, 1) 129 go func() { 130 for { 131 if isProcessRunning(p) { 132 time.Sleep(100 * time.Millisecond) 133 } else { 134 exited <- true 135 return 136 } 137 } 138 }() 139 select { 140 case <-exited: 141 if !p.killTimer.Stop() { 142 <-p.killTimer.C 143 } 144 logger.Debugf(true, "Plugin [%s] with pid [%d] has exited", p.descriptor.Name, p.pluginCmd.Process.Pid) 145 case <-p.killTimer.C: 146 logger.Warningf(true, "Plugin [%s] with pid [%d] did not exit after %.2f seconds. Forcefully killing it.", p.descriptor.Name, p.pluginCmd.Process.Pid, config.PluginKillTimeout().Seconds()) 147 err := p.pluginCmd.Process.Kill() 148 if err != nil { 149 logger.Warningf(true, "Error while killing plugin %s : %s ", p.descriptor.Name, err.Error()) 150 } 151 return err 152 } 153 } 154 return nil 155 } 156 157 // IsPluginInstalled checks if given plugin with specific version is installed or not. 158 func IsPluginInstalled(pluginName, pluginVersion string) bool { 159 pluginsInstallDir, err := common.GetPluginsInstallDir(pluginName) 160 if err != nil { 161 return false 162 } 163 164 thisPluginDir := filepath.Join(pluginsInstallDir, pluginName) 165 if !common.DirExists(thisPluginDir) { 166 return false 167 } 168 169 if pluginVersion != "" { 170 return common.FileExists(filepath.Join(thisPluginDir, pluginVersion, common.PluginJSONFile)) 171 } 172 return true 173 } 174 175 func getPluginJSONPath(pluginName, pluginVersion string) (string, error) { 176 if !IsPluginInstalled(pluginName, pluginVersion) { 177 plugin := strings.TrimSpace(fmt.Sprintf("%s %s", pluginName, pluginVersion)) 178 return "", fmt.Errorf("Plugin %s is not installed", plugin) 179 } 180 181 pluginInstallDir, err := GetInstallDir(pluginName, "") 182 if err != nil { 183 return "", err 184 } 185 return filepath.Join(pluginInstallDir, common.PluginJSONFile), nil 186 } 187 188 // GetPluginDescriptor return the information about the plugin including name, id, commands to start etc. 189 func GetPluginDescriptor(pluginID, pluginVersion string) (*PluginDescriptor, error) { 190 pluginJSON, err := getPluginJSONPath(pluginID, pluginVersion) 191 if err != nil { 192 return nil, err 193 } 194 return GetPluginDescriptorFromJSON(pluginJSON) 195 } 196 197 func GetPluginDescriptorFromJSON(pluginJSON string) (*PluginDescriptor, error) { 198 pluginJSONContents, err := common.ReadFileContents(pluginJSON) 199 if err != nil { 200 return nil, err 201 } 202 var pd PluginDescriptor 203 if err = json.Unmarshal([]byte(pluginJSONContents), &pd); err != nil { 204 return nil, fmt.Errorf("%s: %s", pluginJSON, err.Error()) 205 } 206 pd.pluginPath = filepath.Dir(pluginJSON) 207 208 return &pd, nil 209 } 210 211 func startPlugin(pd *PluginDescriptor, action pluginScope) (*plugin, error) { 212 var command []string 213 switch runtime.GOOS { 214 case "windows": 215 command = pd.Command.Windows 216 case "darwin": 217 command = pd.Command.Darwin 218 default: 219 command = pd.Command.Linux 220 } 221 if len(command) == 0 { 222 return nil, fmt.Errorf("Platform specific command not specified: %s.", runtime.GOOS) 223 } 224 if pd.hasCapability(gRPCSupportCapability) { 225 return startGRPCPlugin(pd, command) 226 } 227 return startLegacyPlugin(pd, command) 228 } 229 230 func startGRPCPlugin(pd *PluginDescriptor, command []string) (*plugin, error) { 231 portChan := make(chan string) 232 writer := &logger.LogWriter{ 233 Stderr: logger.NewCustomWriter(portChan, os.Stderr, pd.ID, true), 234 Stdout: logger.NewCustomWriter(portChan, os.Stdout, pd.ID, false), 235 } 236 cmd, err := common.ExecuteCommand(command, pd.pluginPath, writer.Stdout, writer.Stderr) 237 go func() { 238 err = cmd.Wait() 239 if err != nil { 240 logger.Errorf(true, "Error occurred while waiting for plugin process to finish.\nError : %s", err.Error()) 241 } 242 }() 243 if err != nil { 244 return nil, err 245 } 246 247 var port string 248 select { 249 case port = <-portChan: 250 close(portChan) 251 case <-time.After(config.PluginConnectionTimeout()): 252 return nil, fmt.Errorf("timed out connecting to %s", pd.ID) 253 } 254 logger.Debugf(true, "Attempting to connect to grpc server at port: %s", port) 255 gRPCConn, err := grpc.NewClient(fmt.Sprintf("%s:%s", "127.0.0.1", port), 256 grpc.WithTransportCredentials(insecure.NewCredentials()), 257 grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(1024*1024*1024), grpc.MaxCallRecvMsgSize(1024*1024*1024))) 258 if err != nil { 259 return nil, err 260 } 261 plugin := &plugin{ 262 pluginCmd: cmd, 263 descriptor: pd, 264 gRPCConn: gRPCConn, 265 mutex: &sync.Mutex{}, 266 } 267 if pd.hasScope(docScope) { 268 plugin.DocumenterClient = gauge_messages.NewDocumenterClient(gRPCConn) 269 } else { 270 plugin.ReporterClient = gauge_messages.NewReporterClient(gRPCConn) 271 } 272 273 logger.Debugf(true, "Successfully made the connection with plugin with port: %s", port) 274 return plugin, nil 275 } 276 277 func startLegacyPlugin(pd *PluginDescriptor, command []string) (*plugin, error) { 278 writer := logger.NewLogWriter(pd.ID, true, 0) 279 cmd, err := common.ExecuteCommand(command, pd.pluginPath, writer.Stdout, writer.Stderr) 280 281 if err != nil { 282 return nil, err 283 } 284 var mutex = &sync.Mutex{} 285 go func() { 286 pState, _ := cmd.Process.Wait() 287 mutex.Lock() 288 cmd.ProcessState = pState 289 mutex.Unlock() 290 }() 291 plugin := &plugin{pluginCmd: cmd, descriptor: pd, mutex: mutex} 292 return plugin, nil 293 } 294 295 func SetEnvForPlugin(action pluginScope, pd *PluginDescriptor, m *manifest.Manifest, pluginEnvVars map[string]string) error { 296 pluginEnvVars[fmt.Sprintf("%s_action", pd.ID)] = string(action) 297 pluginEnvVars["test_language"] = m.Language 298 return setEnvironmentProperties(pluginEnvVars) 299 } 300 301 func setEnvironmentProperties(properties map[string]string) error { 302 for k, v := range properties { 303 if err := common.SetEnvVariable(k, v); err != nil { 304 return err 305 } 306 } 307 return nil 308 } 309 310 func IsPluginAdded(m *manifest.Manifest, descriptor *PluginDescriptor) bool { 311 for _, pluginID := range m.Plugins { 312 if pluginID == descriptor.ID { 313 return true 314 } 315 } 316 return false 317 } 318 319 func startPluginsForExecution(m *manifest.Manifest) (Handler, []string) { 320 var warnings []string 321 handler := &GaugePlugins{} 322 envProperties := make(map[string]string) 323 324 for _, pluginID := range m.Plugins { 325 pd, err := GetPluginDescriptor(pluginID, "") 326 if err != nil { 327 warnings = append(warnings, fmt.Sprintf("Unable to start plugin %s. %s. To install, run `gauge install %s`.", pluginID, err.Error(), pluginID)) 328 continue 329 } 330 compatibilityErr := version.CheckCompatibility(version.CurrentGaugeVersion, &pd.GaugeVersionSupport) 331 if compatibilityErr != nil { 332 warnings = append(warnings, fmt.Sprintf("Compatible %s plugin version to current Gauge version %s not found", pd.Name, version.CurrentGaugeVersion)) 333 continue 334 } 335 if pd.hasScope(executionScope) { 336 gaugeConnectionHandler, err := conn.NewGaugeConnectionHandler(0, nil) 337 if err != nil { 338 warnings = append(warnings, err.Error()) 339 continue 340 } 341 envProperties[pluginConnectionPortEnv] = strconv.Itoa(gaugeConnectionHandler.ConnectionPortNumber()) 342 prop, err := common.GetGaugeConfigurationFor(common.GaugePropertiesFile) 343 if err != nil { 344 warnings = append(warnings, fmt.Sprintf("Unable to read Gauge configuration. %s", err.Error())) 345 continue 346 } 347 envProperties["plugin_kill_timeout"] = prop["plugin_kill_timeout"] 348 err = SetEnvForPlugin(executionScope, pd, m, envProperties) 349 if err != nil { 350 warnings = append(warnings, fmt.Sprintf("Error setting environment for plugin %s %s. %s", pd.Name, pd.Version, err.Error())) 351 continue 352 } 353 logger.Debugf(true, "Starting %s plugin", pd.Name) 354 plugin, err := startPlugin(pd, executionScope) 355 if err != nil { 356 warnings = append(warnings, fmt.Sprintf("Error starting plugin %s %s. %s", pd.Name, pd.Version, err.Error())) 357 continue 358 } 359 if plugin.gRPCConn != nil { 360 handler.addPlugin(pluginID, plugin) 361 continue 362 } 363 pluginConnection, err := gaugeConnectionHandler.AcceptConnection(config.PluginConnectionTimeout(), make(chan error)) 364 if err != nil { 365 warnings = append(warnings, fmt.Sprintf("Error starting plugin %s %s. Failed to connect to plugin. %s", pd.Name, pd.Version, err.Error())) 366 err := plugin.pluginCmd.Process.Kill() 367 if err != nil { 368 logger.Errorf(false, "unable to kill plugin %s: %s", plugin.descriptor.Name, err.Error()) 369 } 370 continue 371 } 372 logger.Debugf(true, "Established connection to %s plugin", pd.Name) 373 plugin.connection = pluginConnection 374 handler.addPlugin(pluginID, plugin) 375 } 376 377 } 378 return handler, warnings 379 } 380 381 func GenerateDoc(pluginName string, specDirs []string, startAPIFunc func([]string) int) { 382 pd, err := GetPluginDescriptor(pluginName, "") 383 if err != nil { 384 logger.Fatalf(true, "Error starting plugin %s. Failed to get plugin.json. %s. To install, run `gauge install %s`.", pluginName, err.Error(), pluginName) 385 } 386 if err := version.CheckCompatibility(version.CurrentGaugeVersion, &pd.GaugeVersionSupport); err != nil { 387 logger.Fatalf(true, "Compatible %s plugin version to current Gauge version %s not found", pd.Name, version.CurrentGaugeVersion) 388 } 389 if !pd.hasScope(docScope) { 390 logger.Fatalf(true, "Invalid plugin name: %s, this plugin cannot generate documentation.", pd.Name) 391 } 392 var sources []string 393 for _, src := range specDirs { 394 path, _ := filepath.Abs(src) 395 sources = append(sources, path) 396 } 397 os.Setenv("GAUGE_SPEC_DIRS", strings.Join(sources, "||")) 398 os.Setenv("GAUGE_PROJECT_ROOT", config.ProjectRoot) 399 if pd.hasCapability(gRPCSupportCapability) { 400 p, err := startPlugin(pd, docScope) 401 if err != nil { 402 logger.Fatalf(true, " %s %s. %s", pd.Name, pd.Version, err.Error()) 403 } 404 _, err = p.DocumenterClient.GenerateDocs(context.Background(), getSpecDetails(specDirs)) 405 grpcErr := p.killGrpcProcess() 406 if grpcErr != nil { 407 logger.Errorf(false, "Unable to kill plugin %s : %s", p.descriptor.Name, grpcErr.Error()) 408 } 409 if err != nil { 410 logger.Fatalf(true, "Failed to generate docs. %s", err.Error()) 411 } 412 } else { 413 port := startAPIFunc(specDirs) 414 err := os.Setenv(common.APIPortEnvVariableName, strconv.Itoa(port)) 415 if err != nil { 416 logger.Fatalf(true, "Failed to set env GAUGE_API_PORT. %s", err.Error()) 417 } 418 p, err := startPlugin(pd, docScope) 419 if err != nil { 420 logger.Fatalf(true, " %s %s. %s", pd.Name, pd.Version, err.Error()) 421 } 422 for isProcessRunning(p) { 423 } 424 } 425 } 426 427 func (p *plugin) invokeService(m *gauge_messages.Message) error { 428 ctx := context.Background() 429 var err error 430 switch m.GetMessageType() { 431 case gauge_messages.Message_SuiteExecutionResult: 432 _, err = p.ReporterClient.NotifySuiteResult(ctx, m.GetSuiteExecutionResult()) 433 case gauge_messages.Message_ExecutionStarting: 434 _, err = p.ReporterClient.NotifyExecutionStarting(ctx, m.GetExecutionStartingRequest()) 435 case gauge_messages.Message_ExecutionEnding: 436 _, err = p.ReporterClient.NotifyExecutionEnding(ctx, m.GetExecutionEndingRequest()) 437 case gauge_messages.Message_SpecExecutionEnding: 438 _, err = p.ReporterClient.NotifySpecExecutionEnding(ctx, m.GetSpecExecutionEndingRequest()) 439 case gauge_messages.Message_SpecExecutionStarting: 440 _, err = p.ReporterClient.NotifySpecExecutionStarting(ctx, m.GetSpecExecutionStartingRequest()) 441 case gauge_messages.Message_ScenarioExecutionEnding: 442 _, err = p.ReporterClient.NotifyScenarioExecutionEnding(ctx, m.GetScenarioExecutionEndingRequest()) 443 case gauge_messages.Message_ScenarioExecutionStarting: 444 _, err = p.ReporterClient.NotifyScenarioExecutionStarting(ctx, m.GetScenarioExecutionStartingRequest()) 445 case gauge_messages.Message_StepExecutionEnding: 446 _, err = p.ReporterClient.NotifyStepExecutionEnding(ctx, m.GetStepExecutionEndingRequest()) 447 case gauge_messages.Message_StepExecutionStarting: 448 _, err = p.ReporterClient.NotifyStepExecutionStarting(ctx, m.GetStepExecutionStartingRequest()) 449 case gauge_messages.Message_ConceptExecutionEnding: 450 _, err = p.ReporterClient.NotifyConceptExecutionEnding(ctx, m.GetConceptExecutionEndingRequest()) 451 case gauge_messages.Message_ConceptExecutionStarting: 452 _, err = p.ReporterClient.NotifyConceptExecutionStarting(ctx, m.GetConceptExecutionStartingRequest()) 453 } 454 return err 455 } 456 457 func (p *plugin) sendMessage(message *gauge_messages.Message) error { 458 if p.gRPCConn != nil { 459 return p.invokeService(message) 460 } 461 messageID := common.GetUniqueID() 462 message.MessageId = messageID 463 messageBytes, err := proto.Marshal(message) 464 if err != nil { 465 return err 466 } 467 err = conn.Write(p.connection, messageBytes) 468 if err != nil { 469 return fmt.Errorf("[Warning] Failed to send message to plugin: %s %s", p.descriptor.ID, err.Error()) 470 } 471 return nil 472 } 473 474 func StartPlugins(m *manifest.Manifest) Handler { 475 pluginHandler, warnings := startPluginsForExecution(m) 476 logger.HandleWarningMessages(true, warnings) 477 return pluginHandler 478 } 479 480 func PluginsWithoutScope() (infos []pluginInfo.PluginInfo) { 481 if plugins, err := pluginInfo.GetAllInstalledPluginsWithVersion(); err == nil { 482 for _, p := range plugins { 483 pd, err := GetPluginDescriptor(p.Name, p.Version.String()) 484 if err == nil && !pd.hasAnyScope() { 485 infos = append(infos, p) 486 } 487 } 488 } 489 return 490 } 491 492 // GetInstallDir returns the install directory of given plugin and a given version. 493 func GetInstallDir(pluginName, v string) (string, error) { 494 allPluginsInstallDir, err := common.GetPluginsInstallDir(pluginName) 495 if err != nil { 496 return "", err 497 } 498 pluginDir := filepath.Join(allPluginsInstallDir, pluginName) 499 if v != "" { 500 pluginDir = filepath.Join(pluginDir, v) 501 } else { 502 latestPlugin, err := pluginInfo.GetLatestInstalledPlugin(pluginDir) 503 if err != nil { 504 return "", err 505 } 506 pluginDir = latestPlugin.Path 507 } 508 return pluginDir, nil 509 } 510 511 func GetLanguageJSONFilePath(language string) (string, error) { 512 languageInstallDir, err := GetInstallDir(language, "") 513 if err != nil { 514 return "", err 515 } 516 languageJSON := filepath.Join(languageInstallDir, fmt.Sprintf("%s.json", language)) 517 if !common.FileExists(languageJSON) { 518 return "", fmt.Errorf("Failed to find the implementation for: %s. %s does not exist.", language, languageJSON) 519 } 520 521 return languageJSON, nil 522 } 523 524 func IsLanguagePlugin(plugin string) bool { 525 if _, err := GetLanguageJSONFilePath(plugin); err != nil { 526 return false 527 } 528 return true 529 } 530 531 func QueryParams() string { 532 return fmt.Sprintf("?l=%s&p=%s&o=%s&a=%s", language(), plugins(), runtime.GOOS, runtime.GOARCH) 533 } 534 535 func language() string { 536 if config.ProjectRoot == "" { 537 return "" 538 } 539 m, err := manifest.ProjectManifest() 540 if err != nil { 541 return "" 542 } 543 return m.Language 544 } 545 546 func plugins() string { 547 pluginInfos, err := pluginInfo.GetAllInstalledPluginsWithVersion() 548 if err != nil { 549 return "" 550 } 551 var plugins []string 552 for _, p := range pluginInfos { 553 plugins = append(plugins, p.Name) 554 } 555 return strings.Join(plugins, ",") 556 } 557 558 func getSpecDetails(specDirs []string) *gauge_messages.SpecDetails { 559 sig := &infoGatherer.SpecInfoGatherer{SpecDirs: specDirs} 560 sig.Init() 561 specDetails := make([]*gauge_messages.SpecDetails_SpecDetail, 0) 562 for _, d := range sig.GetAvailableSpecDetails(specDirs) { 563 detail := &gauge_messages.SpecDetails_SpecDetail{} 564 if d.HasSpec() { 565 detail.Spec = gauge.ConvertToProtoSpec(d.Spec) 566 } 567 for _, e := range d.Errs { 568 detail.ParseErrors = append(detail.ParseErrors, &gauge_messages.Error{Type: gauge_messages.Error_PARSE_ERROR, Filename: e.FileName, Message: e.Message, LineNumber: int32(e.LineNo)}) 569 } 570 specDetails = append(specDetails, detail) 571 } 572 return &gauge_messages.SpecDetails{ 573 Details: specDetails, 574 } 575 }