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

     1  package dashboardserver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"log"
     9  	"os"
    10  	"os/exec"
    11  	"syscall"
    12  	"time"
    13  
    14  	"github.com/shirou/gopsutil/process"
    15  	"github.com/spf13/viper"
    16  	"github.com/turbot/steampipe/pkg/constants"
    17  	"github.com/turbot/steampipe/pkg/dashboard/dashboardassets"
    18  	"github.com/turbot/steampipe/pkg/error_helpers"
    19  	"github.com/turbot/steampipe/pkg/filepaths"
    20  	"github.com/turbot/steampipe/pkg/utils"
    21  )
    22  
    23  type ServiceState string
    24  
    25  const (
    26  	ServiceStateRunning       ServiceState = "running"
    27  	ServiceStateError         ServiceState = "error"
    28  	ServiceStateStructVersion              = 20220411
    29  )
    30  
    31  type DashboardServiceState struct {
    32  	State         ServiceState `json:"state"`
    33  	Error         string       `json:"error"`
    34  	Pid           int          `json:"pid"`
    35  	Port          int          `json:"port"`
    36  	ListenType    string       `json:"listen_type"`
    37  	Listen        []string     `json:"listen"`
    38  	StructVersion int64        `json:"struct_version"`
    39  }
    40  
    41  func loadServiceStateFile() (*DashboardServiceState, error) {
    42  	state := &DashboardServiceState{}
    43  	stateBytes, err := os.ReadFile(filepaths.DashboardServiceStateFilePath())
    44  	if err != nil {
    45  		if os.IsNotExist(err) {
    46  			return nil, nil
    47  		}
    48  		return nil, err
    49  	}
    50  	err = json.Unmarshal(stateBytes, state)
    51  	return state, err
    52  }
    53  
    54  func (s *DashboardServiceState) Save() error {
    55  	// set struct version
    56  	s.StructVersion = ServiceStateStructVersion
    57  
    58  	versionFilePath := filepaths.DashboardServiceStateFilePath()
    59  	return s.write(versionFilePath)
    60  }
    61  
    62  func (s *DashboardServiceState) write(path string) error {
    63  	versionFileJSON, err := json.MarshalIndent(s, "", "  ")
    64  	if err != nil {
    65  		log.Println("Error while writing version file", err)
    66  		return err
    67  	}
    68  	return os.WriteFile(path, versionFileJSON, 0644)
    69  }
    70  
    71  func GetDashboardServiceState() (*DashboardServiceState, error) {
    72  	state, err := loadServiceStateFile()
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	if state == nil {
    77  		return nil, nil
    78  	}
    79  	pidExists, err := utils.PidExists(state.Pid)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	if !pidExists {
    84  		return nil, os.Remove(filepaths.DashboardServiceStateFilePath())
    85  	}
    86  	return state, nil
    87  }
    88  
    89  func StopDashboardService(ctx context.Context) error {
    90  	state, err := GetDashboardServiceState()
    91  	if err != nil {
    92  		return err
    93  	}
    94  	if state == nil {
    95  		return nil
    96  	}
    97  	pidExists, err := utils.PidExists(state.Pid)
    98  	if err != nil {
    99  		return err
   100  	}
   101  	if !pidExists {
   102  		return nil
   103  	}
   104  	process, err := process.NewProcessWithContext(ctx, int32(state.Pid))
   105  	if err != nil {
   106  		return err
   107  	}
   108  	err = process.SendSignalWithContext(ctx, syscall.SIGINT)
   109  	if err != nil {
   110  		return err
   111  	}
   112  	return os.Remove(filepaths.DashboardServiceStateFilePath())
   113  }
   114  
   115  // RunForService spanws an execution of the 'steampipe dashboard' command.
   116  // It is used when starting/restarting the steampipe service with the --dashboard flag set
   117  func RunForService(ctx context.Context, serverListen ListenType, serverPort ListenPort) error {
   118  	self, err := os.Executable()
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	// remove the state file (if any)
   124  	os.Remove(filepaths.DashboardServiceStateFilePath())
   125  
   126  	err = dashboardassets.Ensure(ctx)
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	error_helpers.FailOnError(serverPort.IsValid())
   132  	error_helpers.FailOnError(serverListen.IsValid())
   133  
   134  	// NOTE: args must be specified <arg>=<arg val>, as each entry in this array is a separate arg passed to cobra
   135  	args := []string{
   136  		"dashboard",
   137  		fmt.Sprintf("--%s=%s", constants.ArgDashboardListen, string(serverListen)),
   138  		fmt.Sprintf("--%s=%d", constants.ArgDashboardPort, serverPort),
   139  		fmt.Sprintf("--%s=%s", constants.ArgInstallDir, filepaths.SteampipeDir),
   140  		fmt.Sprintf("--%s=%s", constants.ArgModLocation, viper.GetString(constants.ArgModLocation)),
   141  		fmt.Sprintf("--%s=true", constants.ArgServiceMode),
   142  		fmt.Sprintf("--%s=false", constants.ArgInput),
   143  	}
   144  
   145  	for _, variableArg := range viper.GetStringSlice(constants.ArgVariable) {
   146  		args = append(args, fmt.Sprintf("--%s=%s", constants.ArgVariable, variableArg))
   147  	}
   148  
   149  	for _, varFile := range viper.GetStringSlice(constants.ArgVarFile) {
   150  		args = append(args, fmt.Sprintf("--%s=%s", constants.ArgVarFile, varFile))
   151  	}
   152  	cmd := exec.Command(
   153  		self,
   154  		args...,
   155  	)
   156  	cmd.Env = os.Environ()
   157  
   158  	// set group pgid attributes on the command to ensure the process is not shutdown when its parent terminates
   159  	cmd.SysProcAttr = &syscall.SysProcAttr{
   160  		Setpgid:    true,
   161  		Foreground: false,
   162  	}
   163  
   164  	err = cmd.Start()
   165  	if err != nil {
   166  		return err
   167  	}
   168  
   169  	return waitForDashboardService(ctx)
   170  }
   171  
   172  // when started as a service, 'steampipe dashboard' always writes a
   173  // state file in 'internal' with the outcome - even on failures
   174  // this function polls for the file and loads up the error, if any
   175  func waitForDashboardService(ctx context.Context) error {
   176  	utils.LogTime("db.waitForDashboardServerStartup start")
   177  	defer utils.LogTime("db.waitForDashboardServerStartup end")
   178  
   179  	pingTimer := time.NewTicker(constants.ServicePingInterval)
   180  	timeoutAt := time.After(time.Duration(viper.GetInt(constants.ArgDashboardStartTimeout)) * time.Second)
   181  	defer pingTimer.Stop()
   182  
   183  	for {
   184  		select {
   185  		case <-ctx.Done():
   186  			return ctx.Err()
   187  		case <-pingTimer.C:
   188  			// poll for the state file.
   189  			// when it comes up, return it
   190  			state, err := loadServiceStateFile()
   191  			if err != nil {
   192  				if os.IsNotExist(err) {
   193  					// if the file hasn't been generated yet, that means 'dashboard' is still booting up
   194  					continue
   195  				}
   196  				// there was an unexpected error
   197  				return err
   198  			}
   199  
   200  			if state == nil {
   201  				// no state file yet
   202  				continue
   203  			}
   204  
   205  			// check the state file for an error
   206  			if len(state.Error) > 0 {
   207  				// there was an error during start up
   208  				// remove the state file, since we don't need it anymore
   209  				os.Remove(filepaths.DashboardServiceStateFilePath())
   210  				// and return the error from the state file
   211  				return errors.New(state.Error)
   212  			}
   213  
   214  			// we loaded the state and there was no error
   215  			return nil
   216  		case <-timeoutAt:
   217  			return fmt.Errorf("dashboard server startup timed out")
   218  		}
   219  	}
   220  }
   221  
   222  func WriteServiceStateFile(state *DashboardServiceState) error {
   223  	stateBytes, err := json.MarshalIndent(state, "", " ")
   224  	if err != nil {
   225  		return err
   226  	}
   227  	return os.WriteFile(filepaths.DashboardServiceStateFilePath(), stateBytes, 0666)
   228  }