github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/cmd/service.go (about)

     1  package cmd
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"os"
     9  	"os/signal"
    10  	"strings"
    11  	"time"
    12  
    13  	psutils "github.com/shirou/gopsutil/process"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/viper"
    16  	"github.com/turbot/go-kit/helpers"
    17  	"github.com/turbot/steampipe-plugin-sdk/v5/sperr"
    18  	"github.com/turbot/steampipe/pkg/cmdconfig"
    19  	"github.com/turbot/steampipe/pkg/constants"
    20  	"github.com/turbot/steampipe/pkg/dashboard/dashboardserver"
    21  	"github.com/turbot/steampipe/pkg/db/db_local"
    22  	"github.com/turbot/steampipe/pkg/display"
    23  	"github.com/turbot/steampipe/pkg/error_helpers"
    24  	"github.com/turbot/steampipe/pkg/filepaths"
    25  	"github.com/turbot/steampipe/pkg/pluginmanager"
    26  	pb "github.com/turbot/steampipe/pkg/pluginmanager_service/grpc/proto"
    27  	"github.com/turbot/steampipe/pkg/statushooks"
    28  	"github.com/turbot/steampipe/pkg/utils"
    29  )
    30  
    31  func serviceCmd() *cobra.Command {
    32  	var cmd = &cobra.Command{
    33  		Use:   "service [command]",
    34  		Args:  cobra.NoArgs,
    35  		Short: "Steampipe service management",
    36  		Long: `Steampipe service management.
    37  
    38  Run Steampipe as a local service, exposing it as a database endpoint for
    39  connection from any Postgres compatible database client.`,
    40  	}
    41  
    42  	cmd.AddCommand(serviceStartCmd())
    43  	cmd.AddCommand(serviceStatusCmd())
    44  	cmd.AddCommand(serviceStopCmd())
    45  	cmd.AddCommand(serviceRestartCmd())
    46  	cmd.Flags().BoolP(constants.ArgHelp, "h", false, "Help for service")
    47  	return cmd
    48  }
    49  
    50  // handler for service start
    51  func serviceStartCmd() *cobra.Command {
    52  	var cmd = &cobra.Command{
    53  		Use:   "start",
    54  		Args:  cobra.NoArgs,
    55  		Run:   runServiceStartCmd,
    56  		Short: "Start Steampipe in service mode",
    57  		Long: `Start the Steampipe service.
    58  
    59  Run Steampipe as a local service, exposing it as a database endpoint for
    60  connection from any Postgres compatible database client.`,
    61  	}
    62  
    63  	cmdconfig.
    64  		OnCmd(cmd).
    65  		AddModLocationFlag().
    66  		AddBoolFlag(constants.ArgHelp, false, "Help for service start", cmdconfig.FlagOptions.WithShortHand("h")).
    67  		AddIntFlag(constants.ArgDatabasePort, constants.DatabaseDefaultPort, "Database service port").
    68  		AddStringFlag(constants.ArgDatabaseListenAddresses, string(db_local.ListenTypeNetwork), "Accept connections from: `local` (an alias for `localhost` only), `network` (an alias for `*`), or a comma separated list of hosts and/or IP addresses").
    69  		AddStringFlag(constants.ArgServicePassword, "", "Set the database password for this session").
    70  		// default is false and hides the database user password from service start prompt
    71  		AddBoolFlag(constants.ArgServiceShowPassword, false, "View database password for connecting from another machine").
    72  		// dashboard server
    73  		AddBoolFlag(constants.ArgDashboard, false, "Run the dashboard webserver with the service").
    74  		AddStringFlag(constants.ArgDashboardListen, string(dashboardserver.ListenTypeNetwork), "Accept connections from: local (localhost only) or network (open) (dashboard)").
    75  		AddIntFlag(constants.ArgDashboardPort, constants.DashboardServerDefaultPort, "Report server port").
    76  		// foreground enables the service to run in the foreground - till exit
    77  		AddBoolFlag(constants.ArgForeground, false, "Run the service in the foreground").
    78  
    79  		// flags relevant only if the --dashboard arg is used:
    80  		AddStringSliceFlag(constants.ArgVarFile, nil, "Specify an .spvar file containing variable values (only applies if '--dashboard' flag is also set)").
    81  		// NOTE: use StringArrayFlag for ArgVariable, not StringSliceFlag
    82  		// Cobra will interpret values passed to a StringSliceFlag as CSV,
    83  		// where args passed to StringArrayFlag are not parsed and used raw
    84  		AddStringArrayFlag(constants.ArgVariable, nil, "Specify the value of a variable (only applies if '--dashboard' flag is also set)").
    85  
    86  		// hidden flags for internal use
    87  		AddStringFlag(constants.ArgInvoker, string(constants.InvokerService), "Invoked by \"service\" or \"query\"", cmdconfig.FlagOptions.Hidden())
    88  
    89  	return cmd
    90  }
    91  
    92  // serviceStatusCmd :: handler for service status
    93  func serviceStatusCmd() *cobra.Command {
    94  	var cmd = &cobra.Command{
    95  		Use:   "status",
    96  		Args:  cobra.NoArgs,
    97  		Run:   runServiceStatusCmd,
    98  		Short: "Status of the Steampipe service",
    99  		Long: `Status of the Steampipe service.
   100  
   101  Report current status of the Steampipe database service.`,
   102  	}
   103  
   104  	cmdconfig.OnCmd(cmd).
   105  		AddBoolFlag(constants.ArgHelp, false, "Help for service status", cmdconfig.FlagOptions.WithShortHand("h")).
   106  		// default is false and hides the database user password from service start prompt
   107  		AddBoolFlag(constants.ArgServiceShowPassword, false, "View database password for connecting from another machine").
   108  		AddBoolFlag(constants.ArgAll, false, "Bypasses the INSTALL_DIR and reports status of all running steampipe services")
   109  
   110  	return cmd
   111  }
   112  
   113  // handler for service stop
   114  func serviceStopCmd() *cobra.Command {
   115  	cmd := &cobra.Command{
   116  		Use:   "stop",
   117  		Args:  cobra.NoArgs,
   118  		Run:   runServiceStopCmd,
   119  		Short: "Stop Steampipe service",
   120  		Long:  `Stop the Steampipe service.`,
   121  	}
   122  
   123  	cmdconfig.
   124  		OnCmd(cmd).
   125  		AddBoolFlag(constants.ArgHelp, false, "Help for service stop", cmdconfig.FlagOptions.WithShortHand("h")).
   126  		AddBoolFlag(constants.ArgForce, false, "Forces all services to shutdown, releasing all open connections and ports")
   127  
   128  	return cmd
   129  }
   130  
   131  // restarts the database service
   132  func serviceRestartCmd() *cobra.Command {
   133  	var cmd = &cobra.Command{
   134  		Use:   "restart",
   135  		Args:  cobra.NoArgs,
   136  		Run:   runServiceRestartCmd,
   137  		Short: "Restart Steampipe service",
   138  		Long:  `Restart the Steampipe service.`,
   139  	}
   140  
   141  	cmdconfig.
   142  		OnCmd(cmd).
   143  		AddBoolFlag(constants.ArgHelp, false, "Help for service restart", cmdconfig.FlagOptions.WithShortHand("h")).
   144  		AddBoolFlag(constants.ArgForce, false, "Forces the service to restart, releasing all open connections and ports")
   145  
   146  	return cmd
   147  }
   148  
   149  func runServiceStartCmd(cmd *cobra.Command, _ []string) {
   150  	ctx := cmd.Context()
   151  	utils.LogTime("runServiceStartCmd start")
   152  	defer func() {
   153  		utils.LogTime("runServiceStartCmd end")
   154  		if r := recover(); r != nil {
   155  			error_helpers.ShowError(ctx, helpers.ToError(r))
   156  			if exitCode == constants.ExitCodeSuccessful {
   157  				// there was an error and the exitcode
   158  				// was not set to a non-zero value.
   159  				// set it
   160  				exitCode = constants.ExitCodeUnknownErrorPanic
   161  			}
   162  		}
   163  	}()
   164  
   165  	ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, os.Kill)
   166  	defer cancel()
   167  
   168  	listenAddresses := db_local.StartListenType(viper.GetString(constants.ArgDatabaseListenAddresses)).ToListenAddresses()
   169  
   170  	port := viper.GetInt(constants.ArgDatabasePort)
   171  	if port < 1 || port > 65535 {
   172  		exitCode = constants.ExitCodeInsufficientOrWrongInputs
   173  		panic("Invalid port - must be within range (1:65535)")
   174  	}
   175  
   176  	invoker := constants.Invoker(cmdconfig.Viper().GetString(constants.ArgInvoker))
   177  	if invoker.IsValid() != nil {
   178  		exitCode = constants.ExitCodeInsufficientOrWrongInputs
   179  		error_helpers.FailOnError(invoker.IsValid())
   180  	}
   181  
   182  	startResult, dashboardState, dbServiceStarted := startService(ctx, listenAddresses, port, invoker)
   183  	alreadyRunning := !dbServiceStarted
   184  
   185  	printStatus(ctx, startResult.DbState, startResult.PluginManagerState, dashboardState, alreadyRunning)
   186  
   187  	if viper.GetBool(constants.ArgForeground) {
   188  		runServiceInForeground(ctx)
   189  	}
   190  }
   191  
   192  func startService(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) (_ *db_local.StartResult, _ *dashboardserver.DashboardServiceState, dbServiceStarted bool) {
   193  	statushooks.Show(ctx)
   194  	defer statushooks.Done(ctx)
   195  	log.Printf("[TRACE] startService - listenAddresses=%q", listenAddresses)
   196  
   197  	err := db_local.EnsureDBInstalled(ctx)
   198  	if err != nil {
   199  		exitCode = constants.ExitCodeServiceStartupFailure
   200  		error_helpers.FailOnError(err)
   201  	}
   202  
   203  	// start db, refreshing connections
   204  	startResult := startServiceAndRefreshConnections(ctx, listenAddresses, port, invoker)
   205  	if startResult.Status == db_local.ServiceFailedToStart {
   206  		error_helpers.ShowError(ctx, sperr.New("steampipe service failed to start"))
   207  		exitCode = constants.ExitCodeServiceStartupFailure
   208  		return
   209  	}
   210  
   211  	// if the service is already running, then service start should make the service persistent
   212  	if startResult.Status == db_local.ServiceAlreadyRunning {
   213  		// check that we have the same port and listen parameters
   214  		if port != startResult.DbState.Port {
   215  			exitCode = constants.ExitCodeInsufficientOrWrongInputs
   216  			error_helpers.FailOnError(sperr.New("service is already running on port %d - cannot change port while it's running", startResult.DbState.Port))
   217  		}
   218  		if !startResult.DbState.MatchWithGivenListenAddresses(listenAddresses) {
   219  			exitCode = constants.ExitCodeInsufficientOrWrongInputs
   220  			// this messaging assumes that the resolved addresses from the given addresses have not changed while the service is running
   221  			// although this is an edge case, ideally, we should check for the resolved addresses and give the relevant message
   222  			error_helpers.FailOnError(sperr.New("service is already running and listening on %s - cannot change listen address while it's running", strings.Join(startResult.DbState.ResolvedListenAddresses, ", ")))
   223  		}
   224  
   225  		// convert to being invoked by service
   226  		startResult.DbState.Invoker = constants.InvokerService
   227  		err = startResult.DbState.Save()
   228  		if err != nil {
   229  			exitCode = constants.ExitCodeFileSystemAccessFailure
   230  			error_helpers.FailOnErrorWithMessage(err, "service was already running, but could not make it persistent")
   231  		}
   232  	}
   233  
   234  	dbServiceStarted = startResult.Status == db_local.ServiceStarted
   235  
   236  	var dashboardState *dashboardserver.DashboardServiceState
   237  	if viper.GetBool(constants.ArgDashboard) {
   238  		dashboardState, err = dashboardserver.GetDashboardServiceState()
   239  		if err != nil {
   240  			tryToStopServices(ctx)
   241  			exitCode = constants.ExitCodeServiceStartupFailure
   242  			error_helpers.FailOnError(err)
   243  		}
   244  		if dashboardState == nil {
   245  			dashboardState, err = startDashboardServer(ctx)
   246  			if err != nil {
   247  				tryToStopServices(ctx)
   248  				exitCode = constants.ExitCodeServiceStartupFailure
   249  				error_helpers.FailOnError(err)
   250  			}
   251  			dbServiceStarted = true
   252  		}
   253  	}
   254  	return startResult, dashboardState, dbServiceStarted
   255  }
   256  
   257  func startServiceAndRefreshConnections(ctx context.Context, listenAddresses []string, port int, invoker constants.Invoker) *db_local.StartResult {
   258  	startResult := db_local.StartServices(ctx, listenAddresses, port, invoker)
   259  	if startResult.Error != nil {
   260  		exitCode = constants.ExitCodeServiceStartupFailure
   261  		error_helpers.FailOnError(startResult.Error)
   262  	}
   263  
   264  	if startResult.Status == db_local.ServiceStarted {
   265  		// ask the plugin manager to refresh connections
   266  		// this is executed asyncronously by the plugin manager
   267  		// we ignore this error, since RefreshConnections is async and all errors will flow through
   268  		// the notification system
   269  		// we do not expect any I/O errors on this since the PluginManager is running in the same box
   270  		_, _ = startResult.PluginManager.RefreshConnections(&pb.RefreshConnectionsRequest{})
   271  	}
   272  	return startResult
   273  }
   274  
   275  func tryToStopServices(ctx context.Context) {
   276  	// stop db service
   277  	if _, err := db_local.StopServices(ctx, false, constants.InvokerService); err != nil {
   278  		error_helpers.ShowError(ctx, err)
   279  	}
   280  	// stop the dashboard service
   281  	if err := dashboardserver.StopDashboardService(ctx); err != nil {
   282  		error_helpers.ShowError(ctx, err)
   283  	}
   284  }
   285  
   286  func startDashboardServer(ctx context.Context) (*dashboardserver.DashboardServiceState, error) {
   287  	var dashboardState *dashboardserver.DashboardServiceState
   288  	var err error
   289  
   290  	serverPort := dashboardserver.ListenPort(viper.GetInt(constants.ArgDashboardPort))
   291  	serverListen := dashboardserver.ListenType(viper.GetString(constants.ArgDashboardListen))
   292  
   293  	dashboardState, err = dashboardserver.GetDashboardServiceState()
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	if dashboardState == nil {
   299  		// try stopping the previous service
   300  		// StopDashboardService does nothing if the service is not running
   301  		err = dashboardserver.StopDashboardService(ctx)
   302  		if err != nil {
   303  			return nil, err
   304  		}
   305  		// start dashboard service
   306  		err = dashboardserver.RunForService(ctx, serverListen, serverPort)
   307  		if err != nil {
   308  			return nil, err
   309  		}
   310  		// get the updated state
   311  		dashboardState, err = dashboardserver.GetDashboardServiceState()
   312  		if err != nil {
   313  			error_helpers.ShowWarning(fmt.Sprintf("Started Dashboard server, but could not retrieve state: %v", err))
   314  		}
   315  	}
   316  
   317  	return dashboardState, err
   318  }
   319  
   320  func runServiceInForeground(ctx context.Context) {
   321  	fmt.Println("Hit Ctrl+C to stop the service")
   322  
   323  	sigIntChannel := make(chan os.Signal, 1)
   324  	signal.Notify(sigIntChannel, os.Interrupt)
   325  
   326  	checkTimer := time.NewTicker(100 * time.Millisecond)
   327  	defer checkTimer.Stop()
   328  
   329  	var lastCtrlC time.Time
   330  
   331  	for {
   332  		select {
   333  		case <-checkTimer.C:
   334  			// get the current status
   335  			newInfo, err := db_local.GetState()
   336  			if err != nil {
   337  				continue
   338  			}
   339  			if newInfo == nil {
   340  				fmt.Println("Steampipe service stopped.")
   341  				return
   342  			}
   343  		case <-sigIntChannel:
   344  			fmt.Print("\r")
   345  			dashboardserver.StopDashboardService(ctx)
   346  			// if we have received this signal, then the user probably wants to shut down
   347  			// everything. Shutdowns MUST NOT happen in cancellable contexts
   348  			connectedClients, err := db_local.GetClientCount(context.Background())
   349  			if err != nil {
   350  				// report the error in the off chance that there's one
   351  				error_helpers.ShowError(ctx, err)
   352  				return
   353  			}
   354  
   355  			// we know there will be at least 1 client (connectionWatcher)
   356  			if connectedClients.TotalClients > 1 {
   357  				if lastCtrlC.IsZero() || time.Since(lastCtrlC) > 30*time.Second {
   358  					lastCtrlC = time.Now()
   359  					fmt.Println(buildForegroundClientsConnectedMsg())
   360  					continue
   361  				}
   362  			}
   363  			fmt.Println("Stopping Steampipe service.")
   364  			if _, err := db_local.StopServices(ctx, false, constants.InvokerService); err != nil {
   365  				error_helpers.ShowError(ctx, err)
   366  			} else {
   367  				fmt.Println("Steampipe service stopped.")
   368  			}
   369  			return
   370  		}
   371  	}
   372  }
   373  
   374  func runServiceRestartCmd(cmd *cobra.Command, _ []string) {
   375  	ctx := cmd.Context()
   376  	utils.LogTime("runServiceRestartCmd start")
   377  	defer func() {
   378  		utils.LogTime("runServiceRestartCmd end")
   379  		if r := recover(); r != nil {
   380  			error_helpers.ShowError(ctx, helpers.ToError(r))
   381  			if exitCode == constants.ExitCodeSuccessful {
   382  				// there was an error and the exitcode
   383  				// was not set to a non-zero value.
   384  				// set it
   385  				exitCode = constants.ExitCodeUnknownErrorPanic
   386  			}
   387  		}
   388  	}()
   389  
   390  	dbStartResult, currentDashboardState := restartService(ctx)
   391  
   392  	if dbStartResult != nil {
   393  		printStatus(ctx, dbStartResult.DbState, dbStartResult.PluginManagerState, currentDashboardState, false)
   394  	}
   395  }
   396  
   397  func restartService(ctx context.Context) (_ *db_local.StartResult, _ *dashboardserver.DashboardServiceState) {
   398  	statushooks.Show(ctx)
   399  	defer statushooks.Done(ctx)
   400  
   401  	// get current db statue
   402  	currentDbState, err := db_local.GetState()
   403  	error_helpers.FailOnError(err)
   404  	if currentDbState == nil {
   405  		fmt.Println("Steampipe service is not running.")
   406  		return
   407  	}
   408  
   409  	// along with the current dashboard state - maybe nil
   410  	currentDashboardState, err := dashboardserver.GetDashboardServiceState()
   411  	error_helpers.FailOnError(err)
   412  
   413  	// stop db
   414  	stopStatus, err := db_local.StopServices(ctx, viper.GetBool(constants.ArgForce), constants.InvokerService)
   415  	if err != nil {
   416  		exitCode = constants.ExitCodeServiceStopFailure
   417  		error_helpers.FailOnErrorWithMessage(err, "could not stop current instance")
   418  	}
   419  
   420  	if stopStatus != db_local.ServiceStopped {
   421  		fmt.Println(`
   422  Service stop failed.
   423  
   424  Try using:
   425  	steampipe service restart --force
   426  
   427  to force a restart.
   428  		`)
   429  		return
   430  	}
   431  
   432  	// stop the running dashboard server
   433  	err = dashboardserver.StopDashboardService(ctx)
   434  	if err != nil {
   435  		exitCode = constants.ExitCodeServiceStopFailure
   436  		error_helpers.FailOnErrorWithMessage(err, "could not stop dashboard service")
   437  	}
   438  
   439  	// the DB must be installed and therefore is a noop,
   440  	// and EnsureDBInstalled also checks and installs the latest FDW
   441  	err = db_local.EnsureDBInstalled(ctx)
   442  	if err != nil {
   443  		exitCode = constants.ExitCodeServiceStartupFailure
   444  		error_helpers.FailOnError(err)
   445  	}
   446  
   447  	// set the password in 'viper' so that it can be used by 'service start'
   448  	viper.Set(constants.ArgServicePassword, currentDbState.Password)
   449  
   450  	// start db
   451  	dbStartResult := startServiceAndRefreshConnections(ctx, currentDbState.ResolvedListenAddresses, currentDbState.Port, currentDbState.Invoker)
   452  	if dbStartResult.Status == db_local.ServiceFailedToStart {
   453  		exitCode = constants.ExitCodeServiceStartupFailure
   454  		fmt.Println("Steampipe service was stopped, but failed to restart.")
   455  		return
   456  	}
   457  
   458  	// if the dashboard was running, start it
   459  	if currentDashboardState != nil {
   460  		err = dashboardserver.RunForService(ctx, dashboardserver.ListenType(currentDashboardState.ListenType), dashboardserver.ListenPort(currentDashboardState.Port))
   461  		error_helpers.FailOnError(err)
   462  
   463  		// reload the state
   464  		currentDashboardState, err = dashboardserver.GetDashboardServiceState()
   465  		error_helpers.FailOnError(err)
   466  	}
   467  
   468  	return dbStartResult, currentDashboardState
   469  }
   470  
   471  func runServiceStatusCmd(cmd *cobra.Command, _ []string) {
   472  	ctx := cmd.Context()
   473  	utils.LogTime("runServiceStatusCmd status")
   474  	defer func() {
   475  		utils.LogTime("runServiceStatusCmd end")
   476  		if r := recover(); r != nil {
   477  			error_helpers.ShowError(ctx, helpers.ToError(r))
   478  		}
   479  	}()
   480  
   481  	if !db_local.IsDBInstalled() || !db_local.IsFDWInstalled() {
   482  		fmt.Println("Steampipe service is not installed.")
   483  		return
   484  	}
   485  
   486  	if viper.GetBool(constants.ArgAll) {
   487  		showAllStatus(ctx)
   488  	} else {
   489  		dbState, dbStateErr := db_local.GetState()
   490  		pmState, pmStateErr := pluginmanager.LoadState()
   491  		dashboardState, dashboardStateErr := dashboardserver.GetDashboardServiceState()
   492  
   493  		if dbStateErr != nil || pmStateErr != nil {
   494  			error_helpers.ShowError(ctx, composeStateError(dbStateErr, pmStateErr, dashboardStateErr))
   495  			return
   496  		}
   497  		printStatus(ctx, dbState, pmState, dashboardState, false)
   498  	}
   499  }
   500  
   501  func composeStateError(dbStateErr error, pmStateErr error, dashboardStateErr error) error {
   502  	msg := "could not get Steampipe service status:"
   503  
   504  	if dbStateErr != nil {
   505  		msg = fmt.Sprintf(`%s
   506  	failed to get db state: %s`, msg, dbStateErr.Error())
   507  	}
   508  	if pmStateErr != nil {
   509  		msg = fmt.Sprintf(`%s
   510  	failed to get plugin manager state: %s`, msg, pmStateErr.Error())
   511  	}
   512  	if dashboardStateErr != nil {
   513  		msg = fmt.Sprintf(`%s
   514  	failed to get dashboard server state: %s`, msg, pmStateErr.Error())
   515  	}
   516  
   517  	return errors.New(msg)
   518  }
   519  
   520  func runServiceStopCmd(cmd *cobra.Command, _ []string) {
   521  	ctx := cmd.Context()
   522  	utils.LogTime("runServiceStopCmd stop")
   523  
   524  	var status db_local.StopStatus
   525  	var dbStopError error
   526  	var dbState *db_local.RunningDBInstanceInfo
   527  
   528  	defer func() {
   529  		utils.LogTime("runServiceStopCmd end")
   530  		if r := recover(); r != nil {
   531  			error_helpers.ShowError(ctx, helpers.ToError(r))
   532  			if exitCode == constants.ExitCodeSuccessful {
   533  				// there was an error and the exitcode
   534  				// was not set to a non-zero value.
   535  				// set it
   536  				exitCode = constants.ExitCodeUnknownErrorPanic
   537  			}
   538  		}
   539  	}()
   540  
   541  	force := cmdconfig.Viper().GetBool(constants.ArgForce)
   542  	if force {
   543  		dashboardStopError := dashboardserver.StopDashboardService(ctx)
   544  		status, dbStopError = db_local.StopServices(ctx, force, constants.InvokerService)
   545  		dbStopError = error_helpers.CombineErrors(dbStopError, dashboardStopError)
   546  		if dbStopError != nil {
   547  			exitCode = constants.ExitCodeServiceStopFailure
   548  			error_helpers.FailOnError(dbStopError)
   549  		}
   550  	} else {
   551  		dbState, dbStopError = db_local.GetState()
   552  		if dbStopError != nil {
   553  			exitCode = constants.ExitCodeServiceStopFailure
   554  			error_helpers.FailOnErrorWithMessage(dbStopError, "could not stop Steampipe service")
   555  		}
   556  
   557  		dashboardState, err := dashboardserver.GetDashboardServiceState()
   558  		if err != nil {
   559  			exitCode = constants.ExitCodeServiceStopFailure
   560  			error_helpers.FailOnErrorWithMessage(err, "could not stop Steampipe service")
   561  		}
   562  
   563  		if dbState == nil {
   564  			fmt.Println("Steampipe service is not running.")
   565  			return
   566  		}
   567  		if dbState.Invoker != constants.InvokerService {
   568  			printRunningImplicit(dbState.Invoker)
   569  			return
   570  		}
   571  
   572  		if dashboardState != nil {
   573  			err = dashboardserver.StopDashboardService(ctx)
   574  			if err != nil {
   575  				exitCode = constants.ExitCodeServiceStopFailure
   576  				error_helpers.FailOnErrorWithMessage(err, "could not stop dashboard server")
   577  			}
   578  		}
   579  
   580  		// check if there are any connected clients to the service
   581  		connectedClients, err := db_local.GetClientCount(ctx)
   582  		if err != nil {
   583  			exitCode = constants.ExitCodeServiceStopFailure
   584  			error_helpers.FailOnErrorWithMessage(err, "service stop failed")
   585  		}
   586  
   587  		// if there are any clients connected (apart from plugin manager clients), do not exit
   588  		if connectedClients.TotalClients-connectedClients.PluginManagerClients > 0 {
   589  			printClientsConnected()
   590  			return
   591  		}
   592  
   593  		status, err = db_local.StopServices(ctx, false, constants.InvokerService)
   594  		if err != nil {
   595  			exitCode = constants.ExitCodeServiceStopFailure
   596  			error_helpers.FailOnErrorWithMessage(err, "service stop failed")
   597  		}
   598  	}
   599  
   600  	switch status {
   601  	case db_local.ServiceStopped:
   602  		fmt.Println("Steampipe database service stopped.")
   603  	case db_local.ServiceNotRunning:
   604  		fmt.Println("Steampipe service is not running.")
   605  	case db_local.ServiceStopFailed:
   606  		fmt.Println("Could not stop Steampipe service.")
   607  	case db_local.ServiceStopTimedOut:
   608  		fmt.Println(`
   609  Service stop operation timed-out.
   610  
   611  This is probably because other clients are connected to the database service.
   612  
   613  Disconnect all clients, or use
   614  	steampipe service stop --force
   615  
   616  to force a shutdown.
   617  		`)
   618  
   619  	}
   620  }
   621  
   622  func showAllStatus(ctx context.Context) {
   623  	var processes []*psutils.Process
   624  	var err error
   625  
   626  	statushooks.SetStatus(ctx, "Getting details")
   627  	processes, err = db_local.FindAllSteampipePostgresInstances(ctx)
   628  	statushooks.Done(ctx)
   629  
   630  	error_helpers.FailOnError(err)
   631  
   632  	if len(processes) == 0 {
   633  		fmt.Println("There are no steampipe services running.")
   634  		return
   635  	}
   636  	headers := []string{"PID", "Install Directory", "Port", "Listen"}
   637  	rows := [][]string{}
   638  
   639  	for _, process := range processes {
   640  		pid, installDir, port, listen := getServiceProcessDetails(process)
   641  		rows = append(rows, []string{pid, installDir, port, string(listen)})
   642  	}
   643  
   644  	display.ShowWrappedTable(headers, rows, &display.ShowWrappedTableOptions{AutoMerge: false})
   645  }
   646  
   647  func getServiceProcessDetails(process *psutils.Process) (string, string, string, db_local.StartListenType) {
   648  	cmdLine, _ := process.CmdlineSlice()
   649  	installDir := strings.TrimSuffix(cmdLine[0], filepaths.ServiceExecutableRelativeLocation())
   650  	var port string
   651  	var listenType db_local.StartListenType
   652  
   653  	for idx, param := range cmdLine {
   654  		if param == "-p" {
   655  			port = cmdLine[idx+1]
   656  		}
   657  		if strings.HasPrefix(param, "listen_addresses") {
   658  			if strings.Contains(param, "localhost") {
   659  				listenType = db_local.ListenTypeLocal
   660  			} else {
   661  				listenType = db_local.ListenTypeNetwork
   662  			}
   663  		}
   664  	}
   665  
   666  	return fmt.Sprintf("%d", process.Pid), installDir, port, listenType
   667  }
   668  
   669  func printStatus(ctx context.Context, dbState *db_local.RunningDBInstanceInfo, pmState *pluginmanager.State, dashboardState *dashboardserver.DashboardServiceState, alreadyRunning bool) {
   670  	if dbState == nil && !pmState.Running {
   671  		fmt.Println("Service is not running")
   672  		return
   673  	}
   674  
   675  	var statusMessage string
   676  
   677  	prefix := `Steampipe service is running:
   678  `
   679  	if alreadyRunning {
   680  		prefix = `Steampipe service is already running:
   681  `
   682  	}
   683  	suffix := `
   684  Managing the Steampipe service:
   685  
   686    # Get status of the service
   687    steampipe service status
   688  
   689    # View database password for connecting from another machine
   690    steampipe service status --show-password
   691  
   692    # Restart the service
   693    steampipe service restart
   694  
   695    # Stop the service
   696    steampipe service stop
   697  `
   698  
   699  	var connectionStr string
   700  	var password string
   701  	if viper.GetBool(constants.ArgServiceShowPassword) {
   702  		connectionStr = fmt.Sprintf(
   703  			"postgres://%v:%v@%v:%v/%v",
   704  			dbState.User,
   705  			dbState.Password,
   706  			utils.GetFirstListenAddress(dbState.ResolvedListenAddresses),
   707  			dbState.Port,
   708  			dbState.Database,
   709  		)
   710  		password = dbState.Password
   711  	} else {
   712  		connectionStr = fmt.Sprintf(
   713  			"postgres://%v@%v:%v/%v",
   714  			dbState.User,
   715  			utils.GetFirstListenAddress(dbState.ResolvedListenAddresses),
   716  			dbState.Port,
   717  			dbState.Database,
   718  		)
   719  		password = "********* [use --show-password to reveal]"
   720  	}
   721  
   722  	postgresFmt := `
   723  Database:
   724  
   725    Host(s):            %v
   726    Port:               %v
   727    Database:           %v
   728    User:               %v
   729    Password:           %v
   730    Connection string:  %v
   731  `
   732  	postgresMsg := fmt.Sprintf(
   733  		postgresFmt,
   734  		strings.Join(dbState.ResolvedListenAddresses, ", "),
   735  		dbState.Port,
   736  		dbState.Database,
   737  		dbState.User,
   738  		password,
   739  		connectionStr,
   740  	)
   741  
   742  	dashboardMsg := ""
   743  
   744  	if dashboardState != nil {
   745  		browserUrl := fmt.Sprintf("http://%s:%d/", dashboardState.Listen[0], dashboardState.Port)
   746  		dashboardMsg = fmt.Sprintf(`
   747  Dashboard:
   748  
   749    Host(s):  %v
   750    Port:     %v
   751    URL:      %v
   752  `, strings.Join(dashboardState.Listen, ", "), dashboardState.Port, browserUrl)
   753  	}
   754  
   755  	if dbState.Invoker == constants.InvokerService {
   756  		statusMessage = fmt.Sprintf(
   757  			"%s%s%s%s",
   758  			prefix,
   759  			postgresMsg,
   760  			dashboardMsg,
   761  			suffix,
   762  		)
   763  	} else {
   764  		msg := `
   765  Steampipe service was started for an active %s session. The service will exit when all active sessions exit.
   766  
   767  To keep the service running after the %s session completes, use %s.
   768  `
   769  
   770  		statusMessage = fmt.Sprintf(
   771  			msg,
   772  			fmt.Sprintf("steampipe %s", dbState.Invoker),
   773  			dbState.Invoker,
   774  			constants.Bold("steampipe service start"),
   775  		)
   776  	}
   777  
   778  	fmt.Println(statusMessage)
   779  
   780  	if dbState != nil && pmState == nil {
   781  		// the service is running, but the plugin_manager is not running and there's no state file
   782  		// meaning that it cannot be restarted by the FDW
   783  		// it's an ERROR
   784  		error_helpers.ShowError(ctx, sperr.New(`
   785  Service is running, but the Plugin Manager cannot be recovered.
   786  Please use %s to recover the service
   787  `,
   788  			constants.Bold("steampipe service restart"),
   789  		))
   790  	}
   791  }
   792  
   793  func printRunningImplicit(invoker constants.Invoker) {
   794  	fmt.Printf(`
   795  Steampipe service is running exclusively for an active %s session.
   796  
   797  To force stop the service, use %s
   798  
   799  `,
   800  		fmt.Sprintf("steampipe %s", invoker),
   801  		constants.Bold("steampipe service stop --force"),
   802  	)
   803  }
   804  
   805  func printClientsConnected() {
   806  	fmt.Printf(
   807  		`
   808  Cannot stop service since there are clients connected to the service.
   809  
   810  To force stop the service, use %s
   811  
   812  `,
   813  		constants.Bold("steampipe service stop --force"),
   814  	)
   815  }
   816  
   817  func buildForegroundClientsConnectedMsg() string {
   818  	return `
   819  Not shutting down service as there as clients connected.
   820  
   821  To force shutdown, press Ctrl+C again.
   822  	`
   823  }