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 }