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  }