Files
steampipe/pkg/pluginmanager/lifecycle.go

187 lines
7.0 KiB
Go

package pluginmanager
import (
"fmt"
"io"
"log"
"os/exec"
"path/filepath"
"syscall"
"time"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"github.com/spf13/viper"
"github.com/turbot/pipe-fittings/v2/app_specific"
"github.com/turbot/pipe-fittings/v2/constants"
"github.com/turbot/steampipe-plugin-sdk/v5/logging"
"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
"github.com/turbot/steampipe/v2/pkg/pluginmanager_service/grpc"
pb "github.com/turbot/steampipe/v2/pkg/pluginmanager_service/grpc/proto"
pluginshared "github.com/turbot/steampipe/v2/pkg/pluginmanager_service/grpc/shared"
)
// StartNewInstance loads the plugin manager state, stops any previous instance and instantiates a new plugin manager
func StartNewInstance(steampipeExecutablePath string) (*State, error) {
// try to load the plugin manager state
state, err := LoadState()
if err != nil {
log.Printf("[WARN] plugin manager StartNewInstance() - load state failed: %s", err)
return nil, err
}
if state.Running {
log.Printf("[TRACE] plugin manager StartNewInstance() found previous instance of plugin manager still running - stopping it")
// stop the current instance
if err := stop(state); err != nil {
log.Printf("[WARN] failed to stop previous instance of plugin manager: %s", err)
return nil, err
}
}
return start(steampipeExecutablePath)
}
// start plugin manager, without checking it is already running
// we need to be provided with the exe path as we have no way of knowing where the steampipe exe it
// when the plugin mananager is first started by steampipe, we derive the exe path from the running process and
// store it in the plugin manager state file - then if the fdw needs to start the plugin manager it knows how to
func start(steampipeExecutablePath string) (*State, error) {
// first resolve the steampipe executable path to be the actual exe path
// - so that we DO NOT store a symlink in the plugin manager state
// (If steampipe is started via a symlink, if we do not resolve the symlink, the state file will contain the symlink
// which means pluginmanager.State.verifyRunning will return a false negative, i.e. it will think the plugin
// manager is not running, as the exe stored in the state file does not match the actual running process)
resolvedExecutablePath, err := filepath.EvalSymlinks(steampipeExecutablePath)
if err != nil {
log.Printf("[WARN] could not resolve symlink for %s: %s", steampipeExecutablePath, err)
return nil, err
}
// note: we assume the install dir has been assigned to file_paths.app_specific.InstallDir
// - this is done both by the FDW and Steampipe
pluginManagerCmd := exec.Command(resolvedExecutablePath,
"plugin-manager",
"--"+constants.ArgInstallDir, app_specific.InstallDir)
// set attributes on the command to ensure the process is not shutdown when its parent terminates
pluginManagerCmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
}
// discard logging from the plugin manager client (plugin manager logs will still flow through to the log file
// as this is set up in the plugin manager)
logger := logging.NewLogger(&hclog.LoggerOptions{Name: "plugin", Output: io.Discard})
// launch the plugin manager the plugin process
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: pluginshared.Handshake,
Plugins: pluginshared.PluginMap,
Cmd: pluginManagerCmd,
AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
Logger: logger,
StartTimeout: time.Duration(viper.GetInt(constants.ArgPluginStartTimeout)) * time.Second,
})
if _, err := client.Start(); err != nil {
log.Printf("[WARN] plugin manager start() failed to start GRPC client for plugin manager: %s", err)
// attempt to retrieve error message encoded in the plugin stdout
err = sperr.WrapWithMessage(grpc.HandleStartFailure(err), "failed to start plugin manager")
return nil, err
}
// create a plugin manager state.
state := NewState(resolvedExecutablePath, client.ReattachConfig())
log.Printf("[TRACE] start: started plugin manager, pid %d", state.Pid)
// now save the state
if err := state.Save(); err != nil {
return nil, err
}
return state, nil
}
// Stop loads the plugin manager state and if a running instance is found, stop it
func Stop() error {
log.Println("[DEBUG] pluginmanager.Stop start")
defer log.Println("[DEBUG] pluginmanager.Stop end")
// try to load the plugin manager state
state, err := LoadState()
if err != nil {
return err
}
if state == nil || !state.Running {
// nothing to do
return nil
}
return stop(state)
}
// stop the running plugin manager instance
func stop(state *State) error {
log.Println("[DEBUG] pluginmanager.stop start")
defer log.Println("[DEBUG] pluginmanager.stop end")
pluginManager, err := NewPluginManagerClient(state)
if err != nil {
return err
}
log.Printf("[TRACE] pluginManager.Shutdown")
// tell plugin manager to kill all plugins
_, err = pluginManager.Shutdown(&pb.ShutdownRequest{})
if err != nil {
return err
}
log.Printf("[TRACE] pluginManager.Shutdown done")
// kill the underlying client
log.Printf("[TRACE] pluginManager.Shutdown killing raw client")
pluginManager.rawClient.Kill()
log.Printf("[TRACE] pluginManager.Shutdown killed raw client")
// now kill the plugin manager process itself if needed and clear the state file
return state.kill()
}
// GetPluginManager connects to a running plugin manager
func GetPluginManager() (pluginshared.PluginManager, error) {
return getPluginManager(true)
}
// getPluginManager determines whether the plugin manager is running
// if not,and if startIfNeeded is true, it starts the manager
// it then returns a plugin manager client
func getPluginManager(startIfNeeded bool) (pluginshared.PluginManager, error) {
// try to load the plugin manager state
state, err := LoadState()
if err != nil {
log.Printf("[WARN] failed to load plugin manager state: %s", err.Error())
return nil, err
}
// if we did not load it and there was no error, it means the plugin manager is not running
// we cannot start it as we do not know the correct steampipe exe path - which is stored in the state
// this is not expected - we would expect the plugin manager to have been started with the datatbase
if state.Executable == "" {
return nil, fmt.Errorf("plugin manager is not running and there is no state file")
}
if state.Running {
log.Printf("[TRACE] plugin manager is running - returning client")
return NewPluginManagerClient(state)
}
// if the plugin manager is not running, it must have crashed/terminated
log.Printf("[TRACE] GetPluginManager called but plugin manager not running")
// is we are not already recursing, start the plugin manager then recurse back into this function
if startIfNeeded {
log.Printf("[TRACE] calling StartNewInstance()")
// start the plugin manager
if _, err := start(state.Executable); err != nil {
return nil, err
}
// recurse in, setting startIfNeeded to false to avoid further recursion on failure
return getPluginManager(false)
}
// not retrying - just fail
return nil, fmt.Errorf("plugin manager is not running")
}