github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/cmd/state-svc/internal/resolver/resolver.go (about)

     1  package resolver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"os"
     7  	"runtime/debug"
     8  	"sort"
     9  	"strconv"
    10  	"time"
    11  
    12  	"github.com/ActiveState/cli/cmd/state-svc/internal/messages"
    13  	"github.com/ActiveState/cli/cmd/state-svc/internal/rtwatcher"
    14  	genserver "github.com/ActiveState/cli/cmd/state-svc/internal/server/generated"
    15  	"github.com/ActiveState/cli/internal/analytics/client/sync"
    16  	anaConsts "github.com/ActiveState/cli/internal/analytics/constants"
    17  	"github.com/ActiveState/cli/internal/analytics/dimensions"
    18  	"github.com/ActiveState/cli/internal/cache/projectcache"
    19  	"github.com/ActiveState/cli/internal/config"
    20  	"github.com/ActiveState/cli/internal/constants"
    21  	"github.com/ActiveState/cli/internal/errs"
    22  	"github.com/ActiveState/cli/internal/graph"
    23  	"github.com/ActiveState/cli/internal/logging"
    24  	configMediator "github.com/ActiveState/cli/internal/mediators/config"
    25  	"github.com/ActiveState/cli/internal/multilog"
    26  	"github.com/ActiveState/cli/internal/poller"
    27  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    28  	"github.com/ActiveState/cli/internal/updater"
    29  	"github.com/ActiveState/cli/pkg/platform/authentication"
    30  	"github.com/ActiveState/cli/pkg/projectfile"
    31  )
    32  
    33  type Resolver struct {
    34  	cfg            *config.Instance
    35  	messages       *messages.Messages
    36  	updatePoller   *poller.Poller
    37  	authPoller     *poller.Poller
    38  	projectIDCache *projectcache.ID
    39  	an             *sync.Client
    40  	anForClient    *sync.Client // Use separate client for events sent through service so we don't contaminate one with the other
    41  	rtwatch        *rtwatcher.Watcher
    42  	auth           *authentication.Auth
    43  }
    44  
    45  // var _ genserver.ResolverRoot = &Resolver{} // Must implement ResolverRoot
    46  
    47  func New(cfg *config.Instance, an *sync.Client, auth *authentication.Auth) (*Resolver, error) {
    48  	msg, err := messages.New(cfg, auth)
    49  	if err != nil {
    50  		return nil, errs.Wrap(err, "Could not initialize messages")
    51  	}
    52  
    53  	upchecker := updater.NewDefaultChecker(cfg, an)
    54  	pollUpdate := poller.New(1*time.Hour, func() (interface{}, error) {
    55  		logging.Debug("Poller checking for update info")
    56  		return upchecker.CheckFor(constants.ChannelName, "")
    57  	})
    58  
    59  	pollRate := time.Minute.Milliseconds()
    60  	if override := os.Getenv(constants.SvcAuthPollingRateEnvVarName); override != "" {
    61  		overrideInt, err := strconv.ParseInt(override, 10, 64)
    62  		if err != nil {
    63  			return nil, errs.New("Failed to parse svc polling time override: %v", err)
    64  		}
    65  		pollRate = overrideInt
    66  	}
    67  
    68  	pollAuth := poller.New(time.Duration(int64(time.Millisecond)*pollRate), func() (interface{}, error) {
    69  		if auth.SyncRequired() {
    70  			return nil, auth.Sync()
    71  		}
    72  		return nil, nil
    73  	})
    74  
    75  	// Note: source does not matter here, as analytics sent via the resolver have a source
    76  	// (e.g. State Tool or Executor), and that source will be used.
    77  	anForClient := sync.New(anaConsts.SrcStateTool, cfg, auth, nil)
    78  	return &Resolver{
    79  		cfg,
    80  		msg,
    81  		pollUpdate,
    82  		pollAuth,
    83  		projectcache.NewID(),
    84  		an,
    85  		anForClient,
    86  		rtwatcher.New(cfg, anForClient),
    87  		auth,
    88  	}, nil
    89  }
    90  
    91  func (r *Resolver) Close() error {
    92  	r.messages.Close()
    93  	r.updatePoller.Close()
    94  	r.authPoller.Close()
    95  	r.anForClient.Close()
    96  	return r.rtwatch.Close()
    97  }
    98  
    99  // Seems gqlgen supplies this so you can separate your resolver and query resolver logic
   100  // So far no need for this, so we're pointing back at ourselves..
   101  func (r *Resolver) Query() genserver.QueryResolver { return r }
   102  
   103  func (r *Resolver) Version(ctx context.Context) (*graph.Version, error) {
   104  	defer func() { handlePanics(recover(), debug.Stack()) }()
   105  
   106  	r.an.EventWithLabel(anaConsts.CatStateSvc, "endpoint", "Version")
   107  	logging.Debug("Version resolver")
   108  	return &graph.Version{
   109  		State: &graph.StateVersion{
   110  			License:  constants.LibraryLicense,
   111  			Version:  constants.Version,
   112  			Channel:  constants.ChannelName,
   113  			Revision: constants.RevisionHash,
   114  			Date:     constants.Date,
   115  		},
   116  	}, nil
   117  }
   118  
   119  func (r *Resolver) AvailableUpdate(ctx context.Context, desiredChannel, desiredVersion string) (*graph.AvailableUpdate, error) {
   120  	defer func() { handlePanics(recover(), debug.Stack()) }()
   121  
   122  	if desiredChannel == "" {
   123  		desiredChannel = constants.ChannelName
   124  	}
   125  
   126  	r.an.EventWithLabel(anaConsts.CatStateSvc, "endpoint", "AvailableUpdate")
   127  	logging.Debug("AvailableUpdate resolver: %s/%s", desiredChannel, desiredVersion)
   128  	defer logging.Debug("AvailableUpdate done")
   129  
   130  	var (
   131  		avUpdate *updater.AvailableUpdate
   132  		ok       bool
   133  		err      error
   134  	)
   135  
   136  	switch {
   137  	case desiredChannel == constants.ChannelName && desiredVersion == "":
   138  		avUpdate, ok = r.updatePoller.ValueFromCache().(*updater.AvailableUpdate)
   139  		if !ok || avUpdate == nil {
   140  			logging.Debug("No update info in poller cache")
   141  			return nil, nil
   142  		}
   143  
   144  		logging.Debug("Update info pulled from poller cache")
   145  
   146  	default:
   147  		logging.Debug("Update info requested for specific channel/version")
   148  
   149  		upchecker := updater.NewDefaultChecker(r.cfg, r.an)
   150  		avUpdate, err = upchecker.CheckFor(desiredChannel, desiredVersion)
   151  		if err != nil {
   152  			return nil, errs.Wrap(err, "Failed to check for specified channel/version: %s/%s", desiredChannel, desiredVersion)
   153  		}
   154  	}
   155  
   156  	availableUpdate := &graph.AvailableUpdate{
   157  		Version:  avUpdate.Version,
   158  		Channel:  avUpdate.Channel,
   159  		Path:     avUpdate.Path,
   160  		Platform: avUpdate.Platform,
   161  		Sha256:   avUpdate.Sha256,
   162  	}
   163  
   164  	return availableUpdate, nil
   165  }
   166  
   167  func (r *Resolver) Projects(ctx context.Context) ([]*graph.Project, error) {
   168  	defer func() { handlePanics(recover(), debug.Stack()) }()
   169  
   170  	r.an.EventWithLabel(anaConsts.CatStateSvc, "endpoint", "Projects")
   171  	logging.Debug("Projects resolver")
   172  	var projects []*graph.Project
   173  	localConfigProjects := projectfile.GetProjectMapping(r.cfg)
   174  	for ns, locations := range localConfigProjects {
   175  		projects = append(projects, &graph.Project{
   176  			Namespace: ns,
   177  			Locations: locations,
   178  		})
   179  	}
   180  	sort.Slice(projects, func(i, j int) bool {
   181  		return projects[i].Namespace < projects[j].Namespace
   182  	})
   183  
   184  	return projects, nil
   185  }
   186  
   187  func (r *Resolver) AnalyticsEvent(_ context.Context, category, action, source string, _label *string, dimensionsJson string) (*graph.AnalyticsEventResponse, error) {
   188  	defer func() { handlePanics(recover(), debug.Stack()) }()
   189  
   190  	logging.Debug("Analytics event resolver: %s - %s: %s (%s)", category, action, ptr.From(_label, "NIL"), source)
   191  
   192  	label := ""
   193  	if _label != nil {
   194  		label = *_label
   195  	}
   196  
   197  	var dims *dimensions.Values
   198  	if err := json.Unmarshal([]byte(dimensionsJson), &dims); err != nil {
   199  		return &graph.AnalyticsEventResponse{Sent: false}, errs.Wrap(err, "Could not unmarshal")
   200  	}
   201  
   202  	// Resolve the project ID - this is a little awkward since I had to work around an import cycle
   203  	dims.RegisterPreProcessor(func(values *dimensions.Values) error {
   204  		values.ProjectID = nil
   205  		if values.ProjectNameSpace == nil || *values.ProjectNameSpace == "" {
   206  			return nil
   207  		}
   208  		id, err := r.projectIDCache.FromNamespace(*values.ProjectNameSpace, r.auth)
   209  		if err != nil {
   210  			logging.Error("Could not resolve project ID for analytics: %s", errs.JoinMessage(err))
   211  		}
   212  		values.ProjectID = &id
   213  		return nil
   214  	})
   215  
   216  	r.anForClient.EventWithSourceAndLabel(category, action, source, label, dims)
   217  
   218  	return &graph.AnalyticsEventResponse{Sent: true}, nil
   219  }
   220  
   221  func (r *Resolver) ReportRuntimeUsage(_ context.Context, pid int, exec, source string, dimensionsJSON string) (*graph.ReportRuntimeUsageResponse, error) {
   222  	defer func() { handlePanics(recover(), debug.Stack()) }()
   223  
   224  	logging.Debug("Runtime usage resolver: %d - %s", pid, exec)
   225  	var dims *dimensions.Values
   226  	if err := json.Unmarshal([]byte(dimensionsJSON), &dims); err != nil {
   227  		return &graph.ReportRuntimeUsageResponse{Received: false}, errs.Wrap(err, "Could not unmarshal")
   228  	}
   229  
   230  	r.rtwatch.Watch(pid, exec, source, dims)
   231  
   232  	return &graph.ReportRuntimeUsageResponse{Received: true}, nil
   233  }
   234  
   235  func (r *Resolver) CheckMessages(ctx context.Context, command string, flags []string) ([]*graph.MessageInfo, error) {
   236  	defer func() { handlePanics(recover(), debug.Stack()) }()
   237  	logging.Debug("Check messages resolver")
   238  	return r.messages.Check(command, flags)
   239  }
   240  
   241  func (r *Resolver) ConfigChanged(ctx context.Context, key string) (*graph.ConfigChangedResponse, error) {
   242  	defer func() { handlePanics(recover(), debug.Stack()) }()
   243  
   244  	go configMediator.NotifyListeners(key)
   245  	return &graph.ConfigChangedResponse{Received: true}, nil
   246  }
   247  
   248  func (r *Resolver) FetchLogTail(ctx context.Context) (string, error) {
   249  	defer func() { handlePanics(recover(), debug.Stack()) }()
   250  
   251  	return logging.ReadTail(), nil
   252  }
   253  
   254  func (r *Resolver) GetProcessesInUse(ctx context.Context, execDir string) ([]*graph.ProcessInfo, error) {
   255  	defer func() { handlePanics(recover(), debug.Stack()) }()
   256  
   257  	inUse := r.rtwatch.GetProcessesInUse(execDir)
   258  	processes := make([]*graph.ProcessInfo, 0, len(inUse))
   259  	for _, entry := range inUse {
   260  		processes = append(processes, &graph.ProcessInfo{entry.Exec, entry.PID})
   261  	}
   262  	return processes, nil
   263  }
   264  
   265  func handlePanics(recovered interface{}, stack []byte) {
   266  	if recovered != nil {
   267  		multilog.Error("Panic: %v", recovered)
   268  		logging.Debug("Stack: %s", string(stack))
   269  		panic(recovered) // We're only logging the panic, not interrupting it
   270  	}
   271  }