Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions services/monitor/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ package monitor
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/TrueBlocks/trueblocks-chifra/v6/pkg/colors"
)

type CommandExecutor interface {
Expand Down Expand Up @@ -34,6 +38,7 @@ func (e *ChifraExecutor) Execute(ctx context.Context, cmd Command, vars Template
chifraCmd := expandedArgs[0]
chifraArgs := expandedArgs[1:]

fmt.Println(colors.BrightGreen, "output", output, "cmd", strings.Join(expandedArgs, " "), colors.Off)
if output != "" {
dir := filepath.Dir(output)
if dir != "" && dir != "." {
Expand All @@ -51,6 +56,7 @@ func (e *ChifraExecutor) Execute(ctx context.Context, cmd Command, vars Template

cmdStr := fmt.Sprintf("chifra %s %s", chifraCmd, strings.Join(chifraArgs, " "))
_ = cmdStr
fmt.Println(colors.BrightGreen, cmdStr, colors.Off)

return nil
}
Expand All @@ -62,16 +68,29 @@ func NewShellExecutor() *ShellExecutor {
}

func (e *ShellExecutor) Execute(ctx context.Context, cmd Command, vars TemplateVars) error {
start := time.Now()
defer func() {
duration := time.Since(start)
if duration > 30*time.Second {
fmt.Printf("[MONITOR] SLOW: Command %s for %s took %v\n", cmd.ID, vars.Address, duration)
}
}()

expandedArgs := make([]string, len(cmd.Arguments))
for i, arg := range cmd.Arguments {
expandedArgs[i] = ExpandTemplate(arg, vars)
}

output := ExpandTemplate(cmd.Output, vars)
fmt.Println(colors.BrightGreen, cmd, strings.Join(expandedArgs, " "), colors.Off)

shellCmd := exec.CommandContext(ctx, cmd.Command, expandedArgs...)
// Create timeout context for this specific command (5 minutes max per monitor)
cmdCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

if output != "" {
shellCmd := exec.CommandContext(cmdCtx, cmd.Command, expandedArgs...)

if output != "" && output != "/dev/null" {
dir := filepath.Dir(output)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0o755); err != nil {
Expand All @@ -88,8 +107,9 @@ func (e *ShellExecutor) Execute(ctx context.Context, cmd Command, vars TemplateV
shellCmd.Stdout = file
shellCmd.Stderr = file
} else {
shellCmd.Stdout = nil
shellCmd.Stderr = nil
// Discard all output - don't set to nil as that can cause blocking
shellCmd.Stdout = io.Discard
shellCmd.Stderr = io.Discard
}

if err := shellCmd.Run(); err != nil {
Expand Down
6 changes: 3 additions & 3 deletions services/monitor/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func TestShellExecutor_Execute_TemplateExpansion(t *testing.T) {

vars := TemplateVars{
Address: "0xabc123",
Chain: "sepolia",
Chain: "gnosis",
FirstBlock: 100,
LastBlock: 200,
BlockCount: 101,
Expand All @@ -239,7 +239,7 @@ func TestShellExecutor_Execute_TemplateExpansion(t *testing.T) {
t.Fatalf("Execute() unexpected error = %v", err)
}

expectedPath := filepath.Join(tmpDir, "sepolia", "0xabc123.txt")
expectedPath := filepath.Join(tmpDir, "gnosis", "0xabc123.txt")
if _, err := os.Stat(expectedPath); os.IsNotExist(err) {
t.Fatalf("Output file not created at expected path: %s", expectedPath)
}
Expand All @@ -249,7 +249,7 @@ func TestShellExecutor_Execute_TemplateExpansion(t *testing.T) {
t.Fatalf("Failed to read output file: %v", err)
}

expectedContent := "Address: 0xabc123, Chain: sepolia, Blocks: 100-200"
expectedContent := "Address: 0xabc123, Chain: gnosis, Blocks: 100-200"
actualContent := strings.TrimSpace(string(content))
if actualContent != expectedContent {
t.Errorf("Expected output '%s', got '%s'", expectedContent, actualContent)
Expand Down
60 changes: 56 additions & 4 deletions services/monitor/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import (
"strings"

"gopkg.in/yaml.v3"

"github.com/TrueBlocks/trueblocks-chifra/v6/pkg/base"
chifraMonitor "github.com/TrueBlocks/trueblocks-chifra/v6/pkg/monitor"
"github.com/TrueBlocks/trueblocks-chifra/v6/pkg/rpc"
)

type MonitorEntry struct {
Expand Down Expand Up @@ -37,6 +41,13 @@ func ParseWatchlist(path string) ([]MonitorEntry, error) {
scanner := bufio.NewScanner(file)
lineNum := 0

chain := "mainnet"
filename := filepath.Base(path)
if strings.HasPrefix(filename, "watchlist-") && strings.HasSuffix(filename, ".txt") {
chain = strings.TrimSuffix(strings.TrimPrefix(filename, "watchlist-"), ".txt")
}
conn := rpc.TempConnection(chain)

for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
Expand All @@ -51,10 +62,23 @@ func ParseWatchlist(path string) ([]MonitorEntry, error) {
}

parts := strings.Split(line, ",")
address := strings.TrimSpace(parts[0])
addressOrEns := strings.TrimSpace(parts[0])

if !isValidAddress(address) {
return nil, NewWatchlistError(path, lineNum, fmt.Sprintf("invalid address: %s", address))
if !base.IsValidAddress(addressOrEns) {
return nil, NewWatchlistError(path, lineNum, fmt.Sprintf("invalid address: %s", addressOrEns))
}

address := addressOrEns
if strings.HasSuffix(addressOrEns, ".eth") {
if aa, ok := conn.GetEnsAddress(addressOrEns); !ok {
if bb, okok := conn.GetEnsAddress(addressOrEns); !okok {
return []MonitorEntry{}, fmt.Errorf("failed to resolve ENS name: %s", addressOrEns)
} else {
address = bb
}
} else {
address = aa
}
}

entry := MonitorEntry{
Expand Down Expand Up @@ -86,6 +110,27 @@ func ParseWatchlist(path string) ([]MonitorEntry, error) {
return entries, nil
}

// readMonitorState reads the LastScanned value from a monitor file using chifra's monitor package
func readMonitorState(chain, address string) (uint64, error) {
// Create monitor instance
mon, err := chifraMonitor.NewMonitor(chain, base.HexToAddress(address), false)
if err != nil {
return 0, err
}
defer mon.Close()

// Read the monitor header to get LastScanned
if err := mon.ReadMonitorHeader(); err != nil {
// Monitor file doesn't exist yet, start from 0
if os.IsNotExist(err) {
return 0, nil
}
return 0, fmt.Errorf("failed to read monitor header: %v", err)
}

return uint64(mon.LastScanned), nil
}

func DiscoverMonitors(chain string) ([]MonitorEntry, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
Expand Down Expand Up @@ -115,9 +160,16 @@ func DiscoverMonitors(chain string) ([]MonitorEntry, error) {
continue
}

// Read the actual LastScanned value from the monitor file
lastScanned, err := readMonitorState(chain, address)
if err != nil {
// fmt.Printf("[MONITOR] Warning: Could not read state for %s: %v, using 0\n", address, err)
lastScanned = 0
}

monitors = append(monitors, MonitorEntry{
Address: address,
StartingBlock: 0,
StartingBlock: lastScanned,
})
}

Expand Down
Loading