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 }