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

     1  package rtwatcher
     2  
     3  import (
     4  	"encoding/json"
     5  	"os"
     6  	"runtime/debug"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  
    11  	anaConst "github.com/ActiveState/cli/internal/analytics/constants"
    12  	"github.com/ActiveState/cli/internal/analytics/dimensions"
    13  	"github.com/ActiveState/cli/internal/config"
    14  	"github.com/ActiveState/cli/internal/constants"
    15  	"github.com/ActiveState/cli/internal/errs"
    16  	"github.com/ActiveState/cli/internal/logging"
    17  	"github.com/ActiveState/cli/internal/multilog"
    18  	"github.com/ActiveState/cli/internal/rtutils/ptr"
    19  	"github.com/ActiveState/cli/internal/runbits/panics"
    20  )
    21  
    22  const defaultInterval = 1 * time.Minute
    23  const CfgKey = "runtime-watchers"
    24  
    25  type Watcher struct {
    26  	an       analytics
    27  	cfg      *config.Instance
    28  	watching []entry
    29  	stop     chan struct{}
    30  	interval time.Duration
    31  }
    32  
    33  type analytics interface {
    34  	EventWithSource(category, action, source string, dim ...*dimensions.Values)
    35  }
    36  
    37  func New(cfg *config.Instance, an analytics) *Watcher {
    38  	w := &Watcher{an: an, stop: make(chan struct{}, 1), cfg: cfg, interval: defaultInterval}
    39  
    40  	if watchersJson := w.cfg.GetString(CfgKey); watchersJson != "" {
    41  		watchers := []entry{}
    42  		err := json.Unmarshal([]byte(watchersJson), &watchers)
    43  		if err != nil {
    44  			multilog.Error("Could not unmarshal watchersL %s", errs.JoinMessage(err))
    45  		} else {
    46  			w.watching = watchers
    47  		}
    48  	}
    49  
    50  	if v := os.Getenv(constants.HeartbeatIntervalEnvVarName); v != "" {
    51  		vv, err := strconv.Atoi(v)
    52  		if err != nil {
    53  			logging.Warning("Invalid value for %s: %s", constants.HeartbeatIntervalEnvVarName, v)
    54  		} else {
    55  			w.interval = time.Duration(vv) * time.Millisecond
    56  		}
    57  	}
    58  
    59  	go w.ticker(w.check)
    60  	return w
    61  }
    62  
    63  func (w *Watcher) ticker(cb func()) {
    64  	defer func() { panics.LogPanics(recover(), debug.Stack()) }()
    65  
    66  	logging.Debug("Starting watcher ticker with interval %s", w.interval.String())
    67  	ticker := time.NewTicker(w.interval)
    68  	for {
    69  		select {
    70  		case <-ticker.C:
    71  			cb()
    72  		case <-w.stop:
    73  			logging.Debug("Stopping watcher ticker")
    74  			return
    75  		}
    76  	}
    77  }
    78  
    79  func (w *Watcher) check() {
    80  	watching := w.watching[:0]
    81  	for i := range w.watching {
    82  		e := w.watching[i] // Must use index, because we are deleting indexes further down
    83  		running, err := e.IsRunning()
    84  		if err != nil && !errs.Matches(err, &processError{}) {
    85  			multilog.Error("Could not check if runtime process is running: %s", errs.JoinMessage(err))
    86  			// Don't return yet, the conditional below still needs to clear this entry
    87  		}
    88  		if !running {
    89  			logging.Debug("Runtime process %d:%s is not running, removing from watcher", e.PID, e.Exec)
    90  			continue
    91  		}
    92  		watching = append(watching, e)
    93  
    94  		go w.RecordUsage(e)
    95  	}
    96  	w.watching = watching
    97  }
    98  
    99  func (w *Watcher) RecordUsage(e entry) {
   100  	logging.Debug("Recording usage of %s (%d)", e.Exec, e.PID)
   101  	w.an.EventWithSource(anaConst.CatRuntimeUsage, anaConst.ActRuntimeHeartbeat, e.Source, e.Dims)
   102  }
   103  
   104  func (w *Watcher) GetProcessesInUse(execDir string) []entry {
   105  	inUse := make([]entry, 0)
   106  
   107  	execDir = strings.ToLower(execDir) // match case-insensitively
   108  	for _, proc := range w.watching {
   109  		if !strings.Contains(strings.ToLower(proc.Exec), execDir) {
   110  			continue
   111  		}
   112  		isRunning, err := proc.IsRunning()
   113  		if err != nil && !errs.Matches(err, &processError{}) {
   114  			multilog.Error("Could not check if runtime process is running: %s", errs.JoinMessage(err))
   115  			// Any errors should not affect fetching which processes are currently in use. We just won't
   116  			// include this one in the list.
   117  		}
   118  		if !isRunning {
   119  			logging.Debug("Runtime process %d:%s is not running", proc.PID, proc.Exec)
   120  			continue
   121  		}
   122  		inUse = append(inUse, proc) // append a copy
   123  	}
   124  
   125  	return inUse
   126  }
   127  
   128  func (w *Watcher) Close() error {
   129  	logging.Debug("Closing runtime watcher")
   130  
   131  	close(w.stop)
   132  
   133  	if len(w.watching) > 0 {
   134  		watchingJson, err := json.Marshal(w.watching)
   135  		if err != nil {
   136  			return errs.Wrap(err, "Could not marshal watchers")
   137  		}
   138  		return w.cfg.Set(CfgKey, watchingJson)
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func (w *Watcher) Watch(pid int, exec, source string, dims *dimensions.Values) {
   145  	logging.Debug("Watching %s (%d)", exec, pid)
   146  	dims.Sequence = ptr.To(-1) // sequence is meaningless for heartbeat events
   147  	e := entry{pid, exec, source, dims}
   148  	w.watching = append(w.watching, e)
   149  	go w.RecordUsage(e) // initial event
   150  }