mirror of
https://github.com/turbot/steampipe.git
synced 2026-02-22 23:00:16 -05:00
228 lines
6.0 KiB
Go
228 lines
6.0 KiB
Go
package dashboardserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/shirou/gopsutil/process"
|
|
"github.com/spf13/viper"
|
|
"github.com/turbot/steampipe/pkg/constants"
|
|
"github.com/turbot/steampipe/pkg/dashboard/dashboardassets"
|
|
"github.com/turbot/steampipe/pkg/filepaths"
|
|
"github.com/turbot/steampipe/pkg/utils"
|
|
)
|
|
|
|
type ServiceState string
|
|
|
|
const (
|
|
ServiceStateRunning ServiceState = "running"
|
|
ServiceStateError ServiceState = "error"
|
|
ServiceStateStructVersion = 20220411
|
|
)
|
|
|
|
type DashboardServiceState struct {
|
|
State ServiceState `json:"state"`
|
|
Error string `json:"error"`
|
|
Pid int `json:"pid"`
|
|
Port int `json:"port"`
|
|
ListenType string `json:"listen_type"`
|
|
Listen []string `json:"listen"`
|
|
StructVersion int64 `json:"struct_version"`
|
|
}
|
|
|
|
func loadServiceStateFile() (*DashboardServiceState, error) {
|
|
state := &DashboardServiceState{}
|
|
stateBytes, err := os.ReadFile(filepaths.DashboardServiceStateFilePath())
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
err = json.Unmarshal(stateBytes, state)
|
|
return state, err
|
|
}
|
|
|
|
func (s *DashboardServiceState) Save() error {
|
|
// set struct version
|
|
s.StructVersion = ServiceStateStructVersion
|
|
|
|
versionFilePath := filepaths.DashboardServiceStateFilePath()
|
|
return s.write(versionFilePath)
|
|
}
|
|
|
|
func (s *DashboardServiceState) write(path string) error {
|
|
versionFileJSON, err := json.MarshalIndent(s, "", " ")
|
|
if err != nil {
|
|
log.Println("Error while writing version file", err)
|
|
return err
|
|
}
|
|
return os.WriteFile(path, versionFileJSON, 0644)
|
|
}
|
|
|
|
func GetDashboardServiceState() (*DashboardServiceState, error) {
|
|
state, err := loadServiceStateFile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if state == nil {
|
|
return nil, nil
|
|
}
|
|
pidExists, err := utils.PidExists(state.Pid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !pidExists {
|
|
return nil, os.Remove(filepaths.DashboardServiceStateFilePath())
|
|
}
|
|
return state, nil
|
|
}
|
|
|
|
func StopDashboardService(ctx context.Context) error {
|
|
state, err := GetDashboardServiceState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if state == nil {
|
|
return nil
|
|
}
|
|
pidExists, err := utils.PidExists(state.Pid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !pidExists {
|
|
return nil
|
|
}
|
|
process, err := process.NewProcessWithContext(ctx, int32(state.Pid))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = process.SendSignalWithContext(ctx, syscall.SIGINT)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Remove(filepaths.DashboardServiceStateFilePath())
|
|
}
|
|
|
|
// RunForService spanws an execution of the 'steampipe dashboard' command.
|
|
// It is used when starting/restarting the steampipe service with the --dashboard flag set
|
|
func RunForService(ctx context.Context, serverListen ListenType, serverPort ListenPort) error {
|
|
self, err := os.Executable()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// remove the state file (if any)
|
|
os.Remove(filepaths.DashboardServiceStateFilePath())
|
|
|
|
err = dashboardassets.Ensure(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
utils.FailOnError(serverPort.IsValid())
|
|
utils.FailOnError(serverListen.IsValid())
|
|
|
|
// NOTE: args must be specified <arg>=<arg val>, as each entry in this array is a separate arg passed to cobra
|
|
args := []string{
|
|
"dashboard",
|
|
fmt.Sprintf("--%s=%s", constants.ArgDashboardListen, string(serverListen)),
|
|
fmt.Sprintf("--%s=%d", constants.ArgDashboardPort, serverPort),
|
|
fmt.Sprintf("--%s=%s", constants.ArgInstallDir, filepaths.SteampipeDir),
|
|
fmt.Sprintf("--%s=%s", constants.ArgWorkspaceChDir, viper.GetString(constants.ArgWorkspaceChDir)),
|
|
fmt.Sprintf("--%s=true", constants.ArgServiceMode),
|
|
fmt.Sprintf("--%s=false", constants.ArgInput),
|
|
}
|
|
|
|
for _, variableArg := range viper.GetStringSlice(constants.ArgVariable) {
|
|
args = append(args, fmt.Sprintf("--%s=%s", constants.ArgVariable, variableArg))
|
|
}
|
|
|
|
for _, varFile := range viper.GetStringSlice(constants.ArgVarFile) {
|
|
args = append(args, fmt.Sprintf("--%s=%s", constants.ArgVarFile, varFile))
|
|
}
|
|
cmd := exec.Command(
|
|
self,
|
|
args...,
|
|
)
|
|
cmd.Env = os.Environ()
|
|
|
|
// set group pgid attributes on the command to ensure the process is not shutdown when its parent terminates
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Setpgid: true,
|
|
Foreground: false,
|
|
}
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return waitForDashboardService(ctx)
|
|
}
|
|
|
|
// when started as a service, 'steampipe dashboard' always writes a
|
|
// state file in 'internal' with the outcome - even on failures
|
|
// this function polls for the file and loads up the error, if any
|
|
func waitForDashboardService(ctx context.Context) error {
|
|
utils.LogTime("db.waitForDashboardServerStartup start")
|
|
defer utils.LogTime("db.waitForDashboardServerStartup end")
|
|
|
|
pingTimer := time.NewTicker(constants.ServicePingInterval)
|
|
timeoutAt := time.After(constants.ServiceStartTimeout)
|
|
defer pingTimer.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-pingTimer.C:
|
|
// poll for the state file.
|
|
// when it comes up, return it
|
|
state, err := loadServiceStateFile()
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// if the file hasn't been generated yet, that means 'dashboard' is still booting up
|
|
continue
|
|
}
|
|
// there was an unexpected error
|
|
return err
|
|
}
|
|
|
|
if state == nil {
|
|
// no state file yet
|
|
continue
|
|
}
|
|
|
|
// check the state file for an error
|
|
if len(state.Error) > 0 {
|
|
// there was an error during start up
|
|
// remove the state file, since we don't need it anymore
|
|
os.Remove(filepaths.DashboardServiceStateFilePath())
|
|
// and return the error from the state file
|
|
return errors.New(state.Error)
|
|
}
|
|
|
|
// we loaded the state and there was no error
|
|
return nil
|
|
case <-timeoutAt:
|
|
return fmt.Errorf("dashboard server startup timed out")
|
|
}
|
|
}
|
|
}
|
|
|
|
func WriteServiceStateFile(state *DashboardServiceState) error {
|
|
stateBytes, err := json.MarshalIndent(state, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(filepaths.DashboardServiceStateFilePath(), stateBytes, 0666)
|
|
}
|