github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state-svc/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "os" 8 "os/signal" 9 "path" 10 "runtime/debug" 11 "syscall" 12 "time" 13 14 "github.com/ActiveState/cli/cmd/state-svc/autostart" 15 anaSync "github.com/ActiveState/cli/internal/analytics/client/sync" 16 anaConst "github.com/ActiveState/cli/internal/analytics/constants" 17 "github.com/ActiveState/cli/internal/captain" 18 "github.com/ActiveState/cli/internal/config" 19 "github.com/ActiveState/cli/internal/constants" 20 "github.com/ActiveState/cli/internal/errs" 21 "github.com/ActiveState/cli/internal/events" 22 "github.com/ActiveState/cli/internal/installation" 23 "github.com/ActiveState/cli/internal/ipc" 24 "github.com/ActiveState/cli/internal/locale" 25 "github.com/ActiveState/cli/internal/logging" 26 "github.com/ActiveState/cli/internal/multilog" 27 "github.com/ActiveState/cli/internal/output" 28 "github.com/ActiveState/cli/internal/primer" 29 "github.com/ActiveState/cli/internal/rollbar" 30 "github.com/ActiveState/cli/internal/runbits/panics" 31 "github.com/ActiveState/cli/internal/svcctl" 32 "github.com/ActiveState/cli/pkg/platform/authentication" 33 "github.com/inconshreveable/mousetrap" 34 ) 35 36 const ( 37 cmdStart = "start" 38 cmdStop = "stop" 39 cmdStatus = "status" 40 cmdForeground = "foreground" 41 ) 42 43 func main() { 44 var exitCode int 45 46 var cfg *config.Instance 47 defer func() { 48 if panics.HandlePanics(recover(), debug.Stack()) { 49 exitCode = 1 50 } 51 52 if cfg != nil { 53 events.Close("config", cfg.Close) 54 } 55 56 if err := events.WaitForEvents(5*time.Second, rollbar.Wait, authentication.LegacyClose, logging.Close); err != nil { 57 logging.Warning("Failing to wait events") 58 } 59 os.Exit(exitCode) 60 }() 61 62 var err error 63 cfg, err = config.New() 64 if err != nil { 65 multilog.Critical("Could not initialize config: %v", errs.JoinMessage(err)) 66 fmt.Fprintf(os.Stderr, "Could not load config, if this problem persists please reinstall the State Tool. Error: %s\n", errs.JoinMessage(err)) 67 exitCode = 1 68 return 69 } 70 rollbar.SetupRollbar(constants.StateServiceRollbarToken) 71 rollbar.SetConfig(cfg) 72 73 if os.Getenv("VERBOSE") == "true" { 74 logging.CurrentHandler().SetVerbose(true) 75 } 76 77 runErr := run(cfg) 78 if runErr != nil { 79 errMsg := errs.JoinMessage(runErr) 80 if locale.IsInputError(runErr) { 81 logging.Debug("state-svc errored out due to input: %s", errMsg) 82 } else if errs.IsExternalError(runErr) { 83 logging.Debug("state-svc errored out due to external error: %s", errMsg) 84 } else { 85 multilog.Critical("state-svc errored out: %s", errMsg) 86 } 87 88 fmt.Fprintln(os.Stderr, errMsg) 89 exitCode = 1 90 } 91 } 92 93 func run(cfg *config.Instance) error { 94 args := os.Args 95 96 out, err := output.New("", &output.Config{ 97 OutWriter: os.Stdout, 98 ErrWriter: os.Stderr, 99 }) 100 if err != nil { 101 return errs.Wrap(err, "Could not initialize outputer") 102 } 103 104 auth := authentication.New(cfg) 105 an := anaSync.New(anaConst.SrcStateService, cfg, auth, out) 106 defer an.Wait() 107 108 if err := autostart.RegisterConfigListener(cfg); err != nil { 109 return errs.Wrap(err, "Could not register config listener") 110 } 111 112 if mousetrap.StartedByExplorer() { 113 // Allow starting the svc via a double click 114 captain.DisableMousetrap() 115 return runStart(out, "svc-start:mouse") 116 } 117 118 p := primer.New(nil, out, nil, nil, nil, nil, cfg, nil, nil, an) 119 120 showVersion := false 121 cmd := captain.NewCommand( 122 path.Base(os.Args[0]), 123 "", 124 "", 125 p, 126 []*captain.Flag{ 127 { 128 Name: "version", 129 Shorthand: "v", 130 Value: &showVersion, 131 }, 132 }, 133 nil, 134 func(ccmd *captain.Command, args []string) error { 135 if showVersion { 136 vd := installation.VersionData{ 137 "CLI Service", 138 constants.LibraryLicense, 139 constants.Version, 140 constants.ChannelName, 141 constants.RevisionHash, 142 constants.Date, 143 constants.OnCI == "true", 144 } 145 out.Print(locale.T("version_info", vd)) 146 return nil 147 } 148 out.Print(ccmd.UsageText()) 149 return nil 150 }, 151 ) 152 153 var foregroundArgText string 154 var autostart bool 155 156 cmd.AddChildren( 157 captain.NewCommand( 158 cmdStart, 159 "", 160 "Start the ActiveState Service (Background)", 161 p, 162 []*captain.Flag{ 163 {Name: "autostart", Value: &autostart, Hidden: true}, // differentiate between autostart and cli invocation 164 }, 165 nil, 166 func(ccmd *captain.Command, args []string) error { 167 logging.Debug("Running CmdStart") 168 argText := "svc-start:cli" 169 if autostart { 170 argText = "svc-start:auto" 171 } 172 return runStart(out, argText) 173 }, 174 ), 175 captain.NewCommand( 176 cmdStop, 177 "", 178 "Stop the ActiveState Service", 179 p, nil, nil, 180 func(ccmd *captain.Command, args []string) error { 181 logging.Debug("Running CmdStop") 182 return runStop() 183 }, 184 ), 185 captain.NewCommand( 186 cmdStatus, 187 "", 188 "Display the Status of the ActiveState Service", 189 p, nil, nil, 190 func(ccmd *captain.Command, args []string) error { 191 logging.Debug("Running CmdStatus") 192 return runStatus(out) 193 }, 194 ), 195 captain.NewCommand( 196 cmdForeground, 197 "", 198 "Start the ActiveState Service (Foreground)", 199 p, nil, 200 []*captain.Argument{ 201 { 202 Name: "Arg text", 203 Description: "Argument text of calling process to be reported if this application is started too often", 204 Value: &foregroundArgText, 205 }, 206 }, 207 func(ccmd *captain.Command, args []string) error { 208 logging.Debug("Running CmdForeground") 209 if err := auth.Sync(); err != nil { 210 logging.Warning("Could not sync authenticated state: %s", err.Error()) 211 } 212 return runForeground(cfg, an, auth, foregroundArgText) 213 }, 214 ), 215 ) 216 217 return cmd.Execute(args[1:]) 218 } 219 220 func runForeground(cfg *config.Instance, an *anaSync.Client, auth *authentication.Auth, argText string) error { 221 logging.Debug("Running in Foreground") 222 223 ctx, cancel := context.WithCancel(context.Background()) 224 defer cancel() 225 226 logFileName := logging.FileName() 227 logging.Debug("Logging to %q", logging.FilePathFor(logFileName)) 228 stopTimer := logging.StartRotateLogTimer() 229 defer stopTimer() 230 231 p := NewService(ctx, cfg, an, auth, logFileName) 232 233 if argText != "" { 234 argText = fmt.Sprintf(" (invoked by %q)", argText) 235 } 236 237 if err := p.Start(); err != nil { 238 return errs.Wrap(err, "Could not start service"+argText) 239 } 240 241 // Handle sigterm 242 sig := make(chan os.Signal, 1) 243 go func() { 244 defer close(sig) 245 246 select { 247 case oscall, ok := <-sig: 248 if !ok { 249 return 250 } 251 logging.Debug("system call:%+v", oscall) 252 // issue a service shutdown on interrupt 253 cancel() 254 if err := p.Stop(); err != nil { 255 logging.Debug("Service stop failed: %v", err) 256 } 257 case <-ctx.Done(): 258 } 259 }() 260 signal.Notify(sig, os.Interrupt, syscall.SIGTERM) 261 defer signal.Stop(sig) 262 263 p.RunIfNotAuthority(time.Second*3, svcctl.NewDefaultIPCClient(), func(err error) { 264 logging.Debug("This instance is not the authority: %v", err) 265 266 cancel() 267 if err := p.Stop(); err != nil { 268 multilog.Critical("Service stop failed: %v", errs.JoinMessage(err)) 269 } 270 }) 271 272 if err := p.Wait(); err != nil { 273 return errs.Wrap(err, "Failure while waiting for server stop") 274 } 275 276 return nil 277 } 278 279 func runStart(out output.Outputer, argText string) error { 280 if _, err := svcctl.EnsureStartedAndLocateHTTP(argText, out); err != nil { 281 if errors.Is(err, ipc.ErrInUse) { 282 out.Print("A State Service instance is already running in the background.") 283 return nil 284 } 285 return errs.Wrap(err, "Could not start serviceManager") 286 } 287 288 return nil 289 } 290 291 func runStop() error { 292 ipcClient := svcctl.NewDefaultIPCClient() 293 if err := svcctl.StopServer(ipcClient); err != nil { 294 return errs.Wrap(err, "Could not stop serviceManager") 295 } 296 return nil 297 } 298 299 func runStatus(out output.Outputer) error { 300 ipcClient := svcctl.NewDefaultIPCClient() 301 // Don't run in background if we're already running 302 port, err := svcctl.LocateHTTP(ipcClient) 303 if err != nil { 304 return errs.Wrap(err, "Service cannot be reached") 305 } 306 out.Print(fmt.Sprintf("Port: %s", port)) 307 out.Print(fmt.Sprintf("Dashboard: http://127.0.0.1%s", port)) 308 309 logfile, err := svcctl.LogFileName(ipcClient) 310 if err != nil { 311 return errs.Wrap(err, "Service could not locate log file") 312 } 313 out.Print(fmt.Sprintf("Log: %s", logging.FilePathFor(logfile))) 314 315 return nil 316 }