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 }