github.com/TrueBlocks/trueblocks-core/src/apps/chifra@v0.0.0-20241022031540-b362680128f7/pkg/monitor/monitor.go (about)

     1  package monitor
     2  
     3  // Copyright 2021 The TrueBlocks Authors. All rights reserved.
     4  // Use of this source code is governed by a license that can
     5  // be found in the LICENSE file.
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"io/fs"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/base"
    17  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/config"
    18  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/file"
    19  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/index"
    20  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/logger"
    21  	"github.com/TrueBlocks/trueblocks-core/src/apps/chifra/pkg/utils"
    22  )
    23  
    24  // Header is the header of the Monitor file. Note that it's the same width as an types.AppRecord
    25  // therefor one should not change its size
    26  type Header struct {
    27  	Magic       uint16 `json:"-"`
    28  	Unused      bool   `json:"-"`
    29  	Deleted     bool   `json:"deleted,omitempty"`
    30  	LastScanned uint32 `json:"lastScanned,omitempty"`
    31  }
    32  
    33  // Monitor carries information about a Monitor file and its header
    34  type Monitor struct {
    35  	Address base.Address `json:"address"`
    36  	Staged  bool         `json:"-"`
    37  	Chain   string       `json:"-"`
    38  	ReadFp  *os.File     `json:"-"`
    39  	Header
    40  }
    41  
    42  const (
    43  	Ext = ".mon.bin"
    44  )
    45  
    46  // NewMonitor returns a Monitor (but has not yet read in the AppearanceRecords). If 'create' is
    47  // sent, create the Monitor if it does not already exist
    48  func NewMonitor(chain string, addr base.Address, create bool) (Monitor, error) {
    49  	mon := new(Monitor)
    50  	mon.Header = Header{Magic: file.SmallMagicNumber}
    51  	mon.Address = addr
    52  	mon.Chain = chain
    53  	if _, err := mon.Reload(create); err != nil {
    54  		logger.Error(err)
    55  	}
    56  	return *mon, nil
    57  }
    58  
    59  // NewMonitorStaged returns a Monitor whose path is in the 'staging' folder
    60  func NewMonitorStaged(chain, addr string) (Monitor, error) {
    61  	mon := Monitor{
    62  		Header:  Header{Magic: file.SmallMagicNumber},
    63  		Address: base.HexToAddress(addr),
    64  		Chain:   chain,
    65  	}
    66  
    67  	// Note, we are not yet staged, so Path returns the production folder
    68  	prodPath := mon.Path()
    69  	mon.Staged = true
    70  	stagedPath := mon.Path()
    71  
    72  	// either copy the existing monitor or create a new one
    73  	if file.FileExists(prodPath) {
    74  		_, err := file.Copy(stagedPath, prodPath)
    75  		if err != nil {
    76  			return mon, err
    77  		}
    78  	} else {
    79  		err := mon.WriteMonHeader(false, 0, false /* force */)
    80  		if err != nil {
    81  			return mon, err
    82  		}
    83  	}
    84  	return mon, nil
    85  }
    86  
    87  // TODO: Most other Stringer interfaces produce JSON data. Can we switch the polarity of this...
    88  
    89  func (mon Monitor) String() string {
    90  	if mon.Deleted {
    91  		return fmt.Sprintf("%s\t%d\t%d\t%d\t%t", mon.Address.Hex(), mon.Count(), file.FileSize(mon.Path()), mon.LastScanned, mon.Deleted)
    92  	}
    93  	return fmt.Sprintf("%s\t%d\t%d\t%d", mon.Address.Hex(), mon.Count(), file.FileSize(mon.Path()), mon.LastScanned)
    94  }
    95  
    96  // Path returns the path to the Monitor file
    97  func (mon *Monitor) Path() (path string) {
    98  	if mon.Staged {
    99  		path = filepath.Join(config.PathToCache(mon.Chain), "monitors", "staging", mon.Address.Hex()+Ext)
   100  	} else {
   101  		path = filepath.Join(config.PathToCache(mon.Chain), "monitors", mon.Address.Hex()+Ext)
   102  	}
   103  	return
   104  }
   105  
   106  // Reload loads information about the monitor such as the file's size and record count
   107  func (mon *Monitor) Reload(create bool) (int64, error) {
   108  	if create && !file.FileExists(mon.Path()) {
   109  		// Make sure the file exists since we've been told to monitor it
   110  		err := mon.WriteMonHeader(false, 0, false /* force */)
   111  		if err != nil {
   112  			return 0, err
   113  		}
   114  	}
   115  	return mon.Count(), nil
   116  }
   117  
   118  func (mon *Monitor) Count() int64 {
   119  	if file.FileSize(mon.Path()) == 0 {
   120  		return 0
   121  	}
   122  	s := file.FileSize(mon.Path())
   123  	w := int64(index.AppRecordWidth)
   124  	n := s / w
   125  	return n - 1
   126  }
   127  
   128  // IsOpen returns true if the underlying monitor file is opened.
   129  func (mon *Monitor) IsOpen() bool {
   130  	return mon.ReadFp != nil
   131  }
   132  
   133  // Close closes an open Monitor if it's open, does nothing otherwise
   134  func (mon *Monitor) Close() {
   135  	if mon.ReadFp != nil {
   136  		mon.ReadFp.Close()
   137  		mon.ReadFp = nil
   138  	}
   139  }
   140  
   141  // IsDeleted returns true if the monitor has been deleted but not removed
   142  func (mon *Monitor) IsDeleted() bool {
   143  	_ = mon.ReadMonitorHeader()
   144  	return mon.Header.Deleted
   145  }
   146  
   147  // Delete marks the file's delete flag, but does not physically remove the file
   148  func (mon *Monitor) Delete() (prev bool) {
   149  	prev = mon.Deleted
   150  	_ = mon.WriteMonHeader(true, mon.LastScanned, false /* force */)
   151  	mon.Deleted = true
   152  	return
   153  }
   154  
   155  // UnDelete unmarks the file's delete flag
   156  func (mon *Monitor) UnDelete() (prev bool) {
   157  	prev = mon.Deleted
   158  	_ = mon.WriteMonHeader(false, mon.LastScanned, false /* force */)
   159  	mon.Deleted = false
   160  	return
   161  }
   162  
   163  // Remove removes a previously deleted file, does nothing if the file is not deleted
   164  func (mon *Monitor) Remove() (bool, error) {
   165  	if !mon.IsDeleted() {
   166  		return false, errors.New("cannot remove a monitor that is not deleted")
   167  	}
   168  	if mon.Staged {
   169  		file.Remove(mon.Path())
   170  		mon.Staged = false
   171  	}
   172  	file.Remove(mon.Path())
   173  	return !file.FileExists(mon.Path()), nil
   174  }
   175  
   176  // ListWatchedMonitors puts a list of Monitors into the monitorChannel. The list of monitors is
   177  // built from a file called addresses.tsv in the current folder
   178  func ListWatchedMonitors(chain, watchList string, monitorChan chan<- Monitor) {
   179  	defer func() {
   180  		monitorChan <- Monitor{Address: base.NotAMonitor}
   181  	}()
   182  
   183  	logger.Info("Reading address list from", watchList)
   184  	lines := file.AsciiFileToLines(watchList)
   185  	addrMap := make(map[base.Address]bool)
   186  	for _, line := range lines {
   187  		line = utils.StripComments(line)
   188  		addr := base.HexToAddress(line)
   189  		if !addrMap[addr] && !addr.IsZero() && base.IsValidAddress(addr.Hex()) {
   190  			mon, _ := NewMonitor(chain, addr, true /* create */)
   191  			monitorChan <- mon
   192  		}
   193  		addrMap[addr] = true
   194  	}
   195  }
   196  
   197  // ListExistingMonitors puts a list of Monitors into the monitorChannel. The list of monitors is built from
   198  // a file called addresses.tsv in the current folder or, if not present, from existing monitors
   199  func ListExistingMonitors(chain string, monitorChan chan<- Monitor) {
   200  	defer func() {
   201  		monitorChan <- Monitor{Address: base.NotAMonitor}
   202  	}()
   203  
   204  	walkFunc := func(path string, info fs.FileInfo, err error) error {
   205  		if err != nil {
   206  			return err
   207  		}
   208  		if !info.IsDir() && strings.HasSuffix(path, ".mon.bin") {
   209  			addr, _ := base.AddressFromPath(path, ".mon.bin")
   210  			if !addr.IsZero() {
   211  				mon, _ := NewMonitor(chain, addr, true /* create */)
   212  				if strings.Contains(path, "staging") {
   213  					mon.Staged = true
   214  				}
   215  				monitorChan <- mon
   216  			}
   217  		}
   218  		return nil
   219  	}
   220  
   221  	path := filepath.Join(config.PathToCache(chain), "monitors")
   222  	_ = filepath.Walk(path, walkFunc)
   223  }
   224  
   225  var monitorMutex sync.Mutex
   226  
   227  // MoveToProduction moves a previously staged monitor to the monitors folder.
   228  func (mon *Monitor) MoveToProduction() error {
   229  	if !mon.Staged {
   230  		return fmt.Errorf("trying to move monitor that is not staged")
   231  	}
   232  
   233  	before, after, err := mon.RemoveDups()
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	if before != after {
   239  		msg := fmt.Sprintf("%s %d duplicates removed.", mon.Address.Hex(), (before - after))
   240  		logger.Warn(msg)
   241  	}
   242  
   243  	oldPath := mon.Path()
   244  	mon.Staged = false
   245  	monitorMutex.Lock()
   246  	err = os.Rename(oldPath, mon.Path())
   247  	monitorMutex.Unlock()
   248  
   249  	return err
   250  }