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  }