github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/internal/monitors/handle_watch.go (about) 1 // Copyright 2021 The TrueBlocks Authors. All rights reserved. 2 // Use of this source code is governed by a license that can 3 // be found in the LICENSE file. 4 5 package monitorsPkg 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "os" 11 "path/filepath" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base" 17 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/colors" 18 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config" 19 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/file" 20 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger" 21 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/monitor" 22 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/output" 23 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/utils" 24 "github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/validate" 25 ) 26 27 // HandleWatch starts the monitor watcher 28 func (opts *MonitorsOptions) HandleWatch(rCtx *output.RenderCtx) error { 29 opts.Globals.Cache = true 30 scraper := NewScraper(colors.Magenta, "MonitorScraper", opts.Sleep, 0) 31 32 var wg sync.WaitGroup 33 wg.Add(1) 34 // Note that this never returns in normal operation 35 go opts.RunMonitorScraper(&wg, &scraper) 36 wg.Wait() 37 38 return nil 39 } 40 41 // RunMonitorScraper runs continually, never stopping and freshens any existing monitors 42 func (opts *MonitorsOptions) RunMonitorScraper(wg *sync.WaitGroup, s *Scraper) { 43 defer wg.Done() 44 45 chain := opts.Globals.Chain 46 tmpPath := filepath.Join(config.PathToCache(chain), "tmp") 47 48 s.ChangeState(true, tmpPath) 49 50 runCount := uint64(0) 51 for { 52 if !s.Running { 53 s.Pause() 54 55 } else { 56 monitorList := opts.getMonitorList() 57 if len(monitorList) == 0 { 58 logger.Error(validate.Usage("No monitors found. Use 'chifra list' to initialize a monitor.").Error()) 59 return 60 } 61 62 if canceled, err := opts.Refresh(monitorList); err != nil { 63 logger.Error(err) 64 return 65 } else { 66 if canceled { 67 return 68 } 69 } 70 71 runCount++ 72 if opts.RunCount != 0 && runCount >= opts.RunCount { 73 return 74 } 75 76 sleep := opts.Sleep 77 if sleep > 0 { 78 ms := time.Duration(sleep*1000) * time.Millisecond 79 if !opts.Globals.TestMode { 80 logger.Info(fmt.Sprintf("Sleeping for %g seconds", sleep)) 81 } 82 time.Sleep(ms) 83 } 84 } 85 } 86 } 87 88 type Command struct { 89 Fmt string `json:"fmt"` 90 Folder string `json:"folder"` 91 Cmd string `json:"cmd"` 92 Cache bool `json:"cache"` 93 } 94 95 func (c *Command) fileName(addr base.Address) string { 96 return filepath.Join(c.Folder, addr.Hex()+"."+c.Fmt) 97 } 98 99 func (c *Command) resolve(addr base.Address, before, after int64) string { 100 fn := c.fileName(addr) 101 if file.FileExists(fn) { 102 if strings.Contains(c.Cmd, "export") { 103 c.Cmd += fmt.Sprintf(" --first_record %d", uint64(before+1)) 104 c.Cmd += fmt.Sprintf(" --max_records %d", uint64(after-before+1)) // extra space won't hurt 105 } else { 106 c.Cmd += fmt.Sprintf(" %d-%d", before+1, after) 107 } 108 c.Cmd += " --append --no_header" 109 } 110 c.Cmd = strings.Replace(c.Cmd, " ", " ", -1) 111 ret := c.Cmd + " --fmt " + c.Fmt + " --output " + c.fileName(addr) + " " + addr.Hex() 112 if c.Cache { 113 ret += " --cache" 114 } 115 return ret 116 } 117 118 func (c *Command) String() string { 119 b, _ := json.MarshalIndent(c, "", " ") 120 return string(b) 121 } 122 123 func (opts *MonitorsOptions) Refresh(monitors []monitor.Monitor) (bool, error) { 124 theCmds, err := opts.getCommands() 125 if err != nil { 126 return false, err 127 } 128 129 batches := batchSlice[monitor.Monitor](monitors, opts.BatchSize) 130 for i := 0; i < len(batches); i++ { 131 addrs := []base.Address{} 132 countsBefore := []int64{} 133 for _, mon := range batches[i] { 134 addrs = append(addrs, mon.Address) 135 countsBefore = append(countsBefore, mon.Count()) 136 } 137 138 batchSize := int(opts.BatchSize) 139 fmt.Printf("%s%d-%d of %d:%s chifra export --freshen", 140 colors.BrightBlue, 141 i*batchSize, 142 base.Min(((i+1)*batchSize)-1, len(monitors)), 143 len(monitors), 144 colors.Green) 145 for _, addr := range addrs { 146 fmt.Printf(" %s", addr.Hex()) 147 } 148 fmt.Println(colors.Off) 149 150 canceled, err := opts.FreshenMonitorsForWatch(addrs) 151 if canceled || err != nil { 152 return canceled, err 153 } 154 155 for j := 0; j < len(batches[i]); j++ { 156 mon := batches[i][j] 157 countAfter := mon.Count() 158 159 if countAfter > 1000000 { 160 // TODO: Make this value configurable 161 fmt.Println(colors.Red, "Too many transactions for address", mon.Address, colors.Off) 162 continue 163 } 164 165 if countAfter == 0 { 166 continue 167 } 168 169 logger.Info(fmt.Sprintf("Processing item %d in batch %d: %d %d\n", j, i, countsBefore[j], countAfter)) 170 171 for _, cmd := range theCmds { 172 countBefore := countsBefore[j] 173 if countBefore == 0 || countAfter > countBefore { 174 utils.System(cmd.resolve(mon.Address, countBefore, countAfter)) 175 // o := opts 176 // o.Globals.File = "" 177 // _ = o.Globals.PassItOn("acctExport", chain, cmd, []string{}) 178 } else if opts.Globals.Verbose { 179 fmt.Println("No new transactions for", mon.Address.Hex(), "since last run.") 180 } 181 } 182 } 183 } 184 return false, nil 185 } 186 187 func batchSlice[T any](slice []T, batchSize uint64) [][]T { 188 var batches [][]T 189 for i := 0; i < len(slice); i += int(batchSize) { 190 end := i + int(batchSize) 191 if end > len(slice) { 192 end = len(slice) 193 } 194 batches = append(batches, slice[i:end]) 195 } 196 return batches 197 } 198 199 func GetExportFormat(cmd, def string) string { 200 if strings.Contains(cmd, "json") { 201 return "json" 202 } else if strings.Contains(cmd, "txt") { 203 return "txt" 204 } else if strings.Contains(cmd, "csv") { 205 return "csv" 206 } 207 if len(def) > 0 { 208 return def 209 } 210 return "csv" 211 } 212 213 func (opts *MonitorsOptions) cleanLine(lineIn string) (cmd Command, err error) { 214 line := strings.Replace(lineIn, "[{ADDRESS}]", "", -1) 215 if strings.Contains(line, "--fmt") { 216 line = strings.Replace(line, "--fmt", "", -1) 217 line = strings.Replace(line, "json", "", -1) 218 line = strings.Replace(line, "csv", "", -1) 219 line = strings.Replace(line, "txt", "", -1) 220 } 221 line = utils.StripComments(line) 222 if len(line) == 0 { 223 return Command{}, nil 224 } 225 226 folder, err := opts.getOutputFolder(line) 227 if err != nil { 228 return Command{}, err 229 } 230 231 _ = file.EstablishFolder(folder) 232 return Command{Cmd: line, Folder: folder, Fmt: GetExportFormat(lineIn, "csv"), Cache: opts.Globals.Cache}, nil 233 } 234 235 func (opts *MonitorsOptions) getCommands() (ret []Command, err error) { 236 lines := file.AsciiFileToLines(opts.Commands) 237 for _, line := range lines { 238 // orig := line 239 if cmd, err := opts.cleanLine(line); err != nil { 240 return nil, err 241 } else if len(cmd.Cmd) == 0 { 242 continue 243 } else { 244 ret = append(ret, cmd) 245 } 246 } 247 return ret, nil 248 } 249 250 func (opts *MonitorsOptions) getOutputFolder(orig string) (string, error) { 251 okMap := map[string]bool{ 252 "export": true, 253 "list": true, 254 "state": true, 255 "tokens": true, 256 } 257 258 cmdLine := orig 259 parts := strings.Split(strings.Replace(cmdLine, " ", " ", -1), " ") 260 if len(parts) < 1 || parts[0] != "chifra" { 261 s := fmt.Sprintf("Invalid command: %s. Must start with 'chifra'.", strings.Trim(orig, " \t\n\r")) 262 logger.Fatal(s) 263 } 264 if len(parts) < 2 || !okMap[parts[1]] { 265 s := fmt.Sprintf("Invalid command: %s. Must start with 'chifra export', 'chifra list', 'chifra state', or 'chifra tokens'.", orig) 266 logger.Fatal(s) 267 } 268 269 cwd, _ := os.Getwd() 270 cmdLine += " " 271 folder := "unknown" 272 if parts[1] == "export" { 273 if strings.Contains(cmdLine, "-p ") || strings.Contains(cmdLine, "--appearances ") { 274 folder = filepath.Join(cwd, parts[1], "appearances") 275 } else if strings.Contains(cmdLine, "-r ") || strings.Contains(cmdLine, "--receipts ") { 276 folder = filepath.Join(cwd, parts[1], "receipts") 277 } else if strings.Contains(cmdLine, "-l ") || strings.Contains(cmdLine, "--logs ") { 278 folder = filepath.Join(cwd, parts[1], "logs") 279 } else if strings.Contains(cmdLine, "-t ") || strings.Contains(cmdLine, "--traces ") { 280 folder = filepath.Join(cwd, parts[1], "traces") 281 } else if strings.Contains(cmdLine, "-n ") || strings.Contains(cmdLine, "--neighbors ") { 282 folder = filepath.Join(cwd, parts[1], "neighbors") 283 } else if strings.Contains(cmdLine, "-C ") || strings.Contains(cmdLine, "--accounting ") { 284 folder = filepath.Join(cwd, parts[1], "accounting") 285 } else if strings.Contains(cmdLine, "-A ") || strings.Contains(cmdLine, "--statements ") { 286 folder = filepath.Join(cwd, parts[1], "statements") 287 } else if strings.Contains(cmdLine, "-b ") || strings.Contains(cmdLine, "--balances ") { 288 folder = filepath.Join(cwd, parts[1], "balances") 289 } else { 290 folder = filepath.Join(cwd, parts[1], "transactions") 291 } 292 293 } else if parts[1] == "list" { 294 folder = filepath.Join(cwd, parts[1], "appearances") 295 296 } else if parts[1] == "state" { 297 if strings.Contains(cmdLine, "-l ") || strings.Contains(cmdLine, "--call ") { 298 folder = filepath.Join(cwd, parts[1], "calls") 299 } else { 300 folder = filepath.Join(cwd, parts[1], "blocks") 301 } 302 303 } else if parts[1] == "tokens" { 304 if strings.Contains(cmdLine, "-b ") || strings.Contains(cmdLine, "--by_acct ") { 305 folder = filepath.Join(cwd, parts[1], "by_acct") 306 } else { 307 folder = filepath.Join(cwd, parts[1], "blocks") 308 } 309 } 310 311 if strings.Contains(folder, "unknown") { 312 return "", fmt.Errorf("unable to determine output folder for command: %s", cmdLine) 313 } 314 315 if abs, err := filepath.Abs(filepath.Join(opts.Globals.Chain, folder)); err != nil { 316 return "", err 317 } else { 318 return abs, nil 319 } 320 } 321 322 func (opts *MonitorsOptions) getMonitorList() []monitor.Monitor { 323 var monitors []monitor.Monitor 324 325 monitorChan := make(chan monitor.Monitor) 326 go monitor.ListWatchedMonitors(opts.Globals.Chain, opts.Watchlist, monitorChan) 327 328 for result := range monitorChan { 329 switch result.Address { 330 case base.NotAMonitor: 331 logger.Info(fmt.Sprintf("Loaded %d monitors", len(monitors))) 332 close(monitorChan) 333 default: 334 if result.Count() > 500000 { 335 logger.Warn("Ignoring too-large address", result.Address) 336 continue 337 } 338 monitors = append(monitors, result) 339 } 340 } 341 342 return monitors 343 }