github.com/dominant-strategies/go-quai@v0.28.2/quaistats/quaistats.go (about)

     1  // Copyright 2016 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  // Package quaistats implements the network stats reporting service.
    18  package quaistats
    19  
    20  import (
    21  	"bytes"
    22  	"context"
    23  	"crypto/sha256"
    24  	"encoding/hex"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"math/big"
    30  	"net"
    31  	"net/http"
    32  	"runtime"
    33  	"strconv"
    34  	"strings"
    35  	"sync"
    36  	"time"
    37  
    38  	"github.com/dgrijalva/jwt-go"
    39  
    40  	lru "github.com/hashicorp/golang-lru"
    41  	"github.com/shirou/gopsutil/cpu"
    42  	"github.com/shirou/gopsutil/mem"
    43  	"github.com/shirou/gopsutil/process"
    44  
    45  	"os/exec"
    46  
    47  	"github.com/dominant-strategies/go-quai/common"
    48  	"github.com/dominant-strategies/go-quai/consensus"
    49  	"github.com/dominant-strategies/go-quai/core"
    50  	"github.com/dominant-strategies/go-quai/core/types"
    51  	"github.com/dominant-strategies/go-quai/eth/downloader"
    52  	ethproto "github.com/dominant-strategies/go-quai/eth/protocols/eth"
    53  	"github.com/dominant-strategies/go-quai/event"
    54  	"github.com/dominant-strategies/go-quai/log"
    55  	"github.com/dominant-strategies/go-quai/node"
    56  	"github.com/dominant-strategies/go-quai/p2p"
    57  	"github.com/dominant-strategies/go-quai/params"
    58  	"github.com/dominant-strategies/go-quai/rpc"
    59  )
    60  
    61  const (
    62  	// chainHeadChanSize is the size of channel listening to ChainHeadEvent.
    63  	chainHeadChanSize = 10
    64  	chainSideChanSize = 10
    65  
    66  	// reportInterval is the time interval between two reports.
    67  	reportInterval = 15
    68  
    69  	c_alpha           = 8
    70  	c_statsErrorValue = int64(-1)
    71  
    72  	// Max number of stats objects to send in one batch
    73  	c_queueBatchSize uint64 = 10
    74  
    75  	// Number of blocks to include in one batch of transactions
    76  	c_txBatchSize uint64 = 10
    77  
    78  	// Seconds that we want to iterate over (3600s = 1 hr)
    79  	c_windowSize uint64 = 3600
    80  
    81  	// Max number of objects to keep in queue
    82  	c_maxQueueSize int = 100
    83  )
    84  
    85  var (
    86  	chainID9000  = big.NewInt(9000)
    87  	chainID12000 = big.NewInt(12000)
    88  	chainID15000 = big.NewInt(15000)
    89  	chainID17000 = big.NewInt(17000)
    90  	chainID1337  = big.NewInt(1337)
    91  )
    92  
    93  // backend encompasses the bare-minimum functionality needed for quaistats reporting
    94  type backend interface {
    95  	SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
    96  	SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription
    97  	SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Subscription
    98  	CurrentHeader() *types.Header
    99  	TotalLogS(header *types.Header) *big.Int
   100  	HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
   101  	Stats() (pending int, queued int)
   102  	Downloader() *downloader.Downloader
   103  	ChainConfig() *params.ChainConfig
   104  	ProcessingState() bool
   105  }
   106  
   107  // fullNodeBackend encompasses the functionality necessary for a full node
   108  // reporting to quaistats
   109  type fullNodeBackend interface {
   110  	backend
   111  	BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
   112  	BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
   113  	CurrentBlock() *types.Block
   114  }
   115  
   116  // Service implements an Quai netstats reporting daemon that pushes local
   117  // chain statistics up to a monitoring server.
   118  type Service struct {
   119  	server  *p2p.Server // Peer-to-peer server to retrieve networking infos
   120  	backend backend
   121  	engine  consensus.Engine // Consensus engine to retrieve variadic block fields
   122  
   123  	node          string // Name of the node to display on the monitoring page
   124  	pass          string // Password to authorize access to the monitoring page
   125  	host          string // Remote address of the monitoring service
   126  	sendfullstats bool   // Whether the node is sending full stats or not
   127  
   128  	pongCh  chan struct{} // Pong notifications are fed into this channel
   129  	headSub event.Subscription
   130  
   131  	transactionStatsQueue *StatsQueue
   132  	detailStatsQueue      *StatsQueue
   133  	appendTimeStatsQueue  *StatsQueue
   134  
   135  	// After handling a block and potentially adding to the queues, it will notify the sendStats goroutine
   136  	// that stats are ready to be sent
   137  	statsReadyCh chan struct{}
   138  
   139  	blockLookupCache *lru.Cache
   140  
   141  	chainID *big.Int
   142  
   143  	instanceDir string // Path to the node's instance directory
   144  }
   145  
   146  // StatsQueue is a thread-safe queue designed for managing and processing stats data.
   147  //
   148  // The primary objective of the StatsQueue is to provide a safe mechanism for enqueuing,
   149  // dequeuing, and requeuing stats objects concurrently across multiple goroutines.
   150  //
   151  // Key Features:
   152  //   - Enqueue: Allows adding an item to the end of the queue.
   153  //   - Dequeue: Removes and returns the item from the front of the queue.
   154  //   - RequeueFront: Adds an item back to the front of the queue, useful for failed processing attempts.
   155  //
   156  // Concurrent Access:
   157  //   - The internal state of the queue is protected by a mutex to prevent data races and ensure
   158  //     that the operations are atomic. As a result, it's safe to use across multiple goroutines
   159  //     without external synchronization.
   160  type StatsQueue struct {
   161  	data  []interface{}
   162  	mutex sync.Mutex
   163  }
   164  
   165  func NewStatsQueue() *StatsQueue {
   166  	return &StatsQueue{
   167  		data: make([]interface{}, 0),
   168  	}
   169  }
   170  
   171  func (q *StatsQueue) Enqueue(item interface{}) {
   172  	q.mutex.Lock()
   173  	defer q.mutex.Unlock()
   174  
   175  	if len(q.data) >= c_maxQueueSize {
   176  		q.dequeue()
   177  	}
   178  
   179  	q.data = append(q.data, item)
   180  }
   181  
   182  func (q *StatsQueue) Dequeue() interface{} {
   183  	q.mutex.Lock()
   184  	defer q.mutex.Unlock()
   185  
   186  	if len(q.data) == 0 {
   187  		return nil
   188  	}
   189  
   190  	return q.dequeue()
   191  }
   192  
   193  func (q *StatsQueue) dequeue() interface{} {
   194  	item := q.data[0]
   195  	q.data = q.data[1:]
   196  	return item
   197  }
   198  
   199  func (q *StatsQueue) EnqueueFront(item interface{}) {
   200  	q.mutex.Lock()
   201  	defer q.mutex.Unlock()
   202  
   203  	if len(q.data) >= c_maxQueueSize {
   204  		q.dequeue()
   205  	}
   206  
   207  	q.data = append([]interface{}{item}, q.data...)
   208  }
   209  
   210  func (q *StatsQueue) EnqueueFrontBatch(items []interface{}) {
   211  	q.mutex.Lock()
   212  	defer q.mutex.Unlock()
   213  
   214  	// Iterate backwards through the items and add them to the front of the queue
   215  	// Going backwards means oldest items are not added in case of overflow
   216  	for i := len(items) - 1; i >= 0; i-- {
   217  		if len(q.data) >= c_maxQueueSize {
   218  			break
   219  		}
   220  		q.data = append([]interface{}{items[i]}, q.data...)
   221  	}
   222  }
   223  
   224  func (q *StatsQueue) Size() int {
   225  	q.mutex.Lock()
   226  	defer q.mutex.Unlock()
   227  
   228  	return len(q.data)
   229  }
   230  
   231  // parseEthstatsURL parses the netstats connection url.
   232  // URL argument should be of the form <nodename:secret@host:port>
   233  // If non-erroring, the returned slice contains 3 elements: [nodename, pass, host]
   234  func parseEthstatsURL(url string) (parts []string, err error) {
   235  	err = fmt.Errorf("invalid netstats url: \"%s\", should be nodename:secret@host:port", url)
   236  
   237  	hostIndex := strings.LastIndex(url, "@")
   238  	if hostIndex == -1 || hostIndex == len(url)-1 {
   239  		return nil, err
   240  	}
   241  	preHost, host := url[:hostIndex], url[hostIndex+1:]
   242  
   243  	passIndex := strings.LastIndex(preHost, ":")
   244  	if passIndex == -1 {
   245  		return []string{preHost, "", host}, nil
   246  	}
   247  	nodename, pass := preHost[:passIndex], ""
   248  	if passIndex != len(preHost)-1 {
   249  		pass = preHost[passIndex+1:]
   250  	}
   251  
   252  	return []string{nodename, pass, host}, nil
   253  }
   254  
   255  // New returns a monitoring service ready for stats reporting.
   256  func New(node *node.Node, backend backend, engine consensus.Engine, url string, sendfullstats bool) error {
   257  	parts, err := parseEthstatsURL(url)
   258  	if err != nil {
   259  		return err
   260  	}
   261  
   262  	chainID := backend.ChainConfig().ChainID
   263  	var durationLimit *big.Int
   264  
   265  	switch {
   266  	case chainID.Cmp(chainID9000) == 0:
   267  		durationLimit = params.DurationLimit
   268  	case chainID.Cmp(chainID12000) == 0:
   269  		durationLimit = params.GardenDurationLimit
   270  	case chainID.Cmp(chainID15000) == 0:
   271  		durationLimit = params.OrchardDurationLimit
   272  	case chainID.Cmp(chainID17000) == 0:
   273  		durationLimit = params.LighthouseDurationLimit
   274  	case chainID.Cmp(chainID1337) == 0:
   275  		durationLimit = params.LocalDurationLimit
   276  	default:
   277  		durationLimit = params.DurationLimit
   278  	}
   279  
   280  	durationLimitInt := durationLimit.Uint64()
   281  
   282  	c_blocksPerWindow := c_windowSize / durationLimitInt
   283  
   284  	blockLookupCache, _ := lru.New(int(c_blocksPerWindow * 2))
   285  
   286  	quaistats := &Service{
   287  		backend:               backend,
   288  		engine:                engine,
   289  		server:                node.Server(),
   290  		node:                  parts[0],
   291  		pass:                  parts[1],
   292  		host:                  parts[2],
   293  		pongCh:                make(chan struct{}),
   294  		chainID:               backend.ChainConfig().ChainID,
   295  		transactionStatsQueue: NewStatsQueue(),
   296  		detailStatsQueue:      NewStatsQueue(),
   297  		appendTimeStatsQueue:  NewStatsQueue(),
   298  		statsReadyCh:          make(chan struct{}),
   299  		sendfullstats:         sendfullstats,
   300  		blockLookupCache:      blockLookupCache,
   301  		instanceDir:           node.InstanceDir(),
   302  	}
   303  
   304  	node.RegisterLifecycle(quaistats)
   305  	return nil
   306  }
   307  
   308  // Start implements node.Lifecycle, starting up the monitoring and reporting daemon.
   309  func (s *Service) Start() error {
   310  	// Subscribe to chain events to execute updates on
   311  	chainHeadCh := make(chan core.ChainHeadEvent, chainHeadChanSize)
   312  
   313  	s.headSub = s.backend.SubscribeChainHeadEvent(chainHeadCh)
   314  
   315  	go s.loopBlocks(chainHeadCh)
   316  	go s.loopSender(s.initializeURLMap())
   317  
   318  	log.Info("Stats daemon started")
   319  	return nil
   320  }
   321  
   322  // Stop implements node.Lifecycle, terminating the monitoring and reporting daemon.
   323  func (s *Service) Stop() error {
   324  	s.headSub.Unsubscribe()
   325  	log.Info("Stats daemon stopped")
   326  	return nil
   327  }
   328  
   329  func (s *Service) loopBlocks(chainHeadCh chan core.ChainHeadEvent) {
   330  	defer func() {
   331  		if r := recover(); r != nil {
   332  			log.Error("Stats process crashed", "error", r)
   333  			go s.loopBlocks(chainHeadCh)
   334  		}
   335  	}()
   336  
   337  	quitCh := make(chan struct{})
   338  
   339  	go func() {
   340  		for {
   341  			select {
   342  			case head := <-chainHeadCh:
   343  				// Directly handle the block
   344  				go s.handleBlock(head.Block)
   345  			case <-s.headSub.Err():
   346  				close(quitCh)
   347  				return
   348  			}
   349  		}
   350  	}()
   351  
   352  	// Wait for the goroutine to signal completion or error
   353  	<-quitCh
   354  }
   355  
   356  // loop keeps trying to connect to the netstats server, reporting chain events
   357  // until termination.
   358  func (s *Service) loopSender(urlMap map[string]string) {
   359  	defer func() {
   360  		if r := recover(); r != nil {
   361  			fmt.Println("Stats process crashed with error:", r)
   362  			go s.loopSender(urlMap)
   363  		}
   364  	}()
   365  
   366  	// Start a goroutine that exhausts the subscriptions to avoid events piling up
   367  	var (
   368  		quitCh = make(chan struct{})
   369  	)
   370  
   371  	nodeStatsMod := 0
   372  
   373  	errTimer := time.NewTimer(0)
   374  	defer errTimer.Stop()
   375  	var authJwt = ""
   376  	// Loop reporting until termination
   377  	for {
   378  		select {
   379  		case <-quitCh:
   380  			return
   381  		case <-errTimer.C:
   382  			// If we don't have a JWT or it's expired, get a new one
   383  			isJwtExpiredResult, jwtIsExpiredErr := s.isJwtExpired(authJwt)
   384  			if authJwt == "" || isJwtExpiredResult || jwtIsExpiredErr != nil {
   385  				log.Info("Trying to login to quaistats")
   386  				var err error
   387  				authJwt, err = s.login2(urlMap["login"])
   388  				if err != nil {
   389  					log.Warn("Stats login failed", "err", err)
   390  					errTimer.Reset(10 * time.Second)
   391  					continue
   392  				}
   393  			}
   394  
   395  			errs := make(map[string]error)
   396  
   397  			// Authenticate the client with the server
   398  			for key, url := range urlMap {
   399  				switch key {
   400  				case "login":
   401  					continue
   402  				case "nodeStats":
   403  					if errs[key] = s.reportNodeStats(url, 0, authJwt); errs[key] != nil {
   404  						log.Warn("Initial stats report failed for "+key, "err", errs[key])
   405  						errTimer.Reset(0)
   406  						continue
   407  					}
   408  				case "blockTransactionStats":
   409  					if errs[key] = s.sendTransactionStats(url, authJwt); errs[key] != nil {
   410  						log.Warn("Initial stats report failed for "+key, "err", errs[key])
   411  						errTimer.Reset(0)
   412  						continue
   413  					}
   414  				case "blockDetailStats":
   415  					if errs[key] = s.sendDetailStats(url, authJwt); errs[key] != nil {
   416  						log.Warn("Initial stats report failed for "+key, "err", errs[key])
   417  						errTimer.Reset(0)
   418  						continue
   419  					}
   420  				case "blockAppendTime":
   421  					if errs[key] = s.sendAppendTimeStats(url, authJwt); errs[key] != nil {
   422  						log.Warn("Initial stats report failed for "+key, "err", errs[key])
   423  						errTimer.Reset(0)
   424  						continue
   425  					}
   426  				}
   427  			}
   428  
   429  			// Keep sending status updates until the connection breaks
   430  			fullReport := time.NewTicker(reportInterval * time.Second)
   431  
   432  			var noErrs = true
   433  			for noErrs {
   434  				var err error
   435  				select {
   436  				case <-quitCh:
   437  					fullReport.Stop()
   438  					return
   439  
   440  				case <-fullReport.C:
   441  					nodeStatsMod ^= 1
   442  					if err = s.reportNodeStats(urlMap["nodeStats"], nodeStatsMod, authJwt); err != nil {
   443  						noErrs = false
   444  						log.Warn("nodeStats full stats report failed", "err", err)
   445  					}
   446  				case <-s.statsReadyCh:
   447  					if url, ok := urlMap["blockTransactionStats"]; ok {
   448  						if err := s.sendTransactionStats(url, authJwt); err != nil {
   449  							noErrs = false
   450  							errTimer.Reset(0)
   451  							log.Warn("blockTransactionStats stats report failed", "err", err)
   452  						}
   453  					}
   454  					if url, ok := urlMap["blockDetailStats"]; ok {
   455  						if err := s.sendDetailStats(url, authJwt); err != nil {
   456  							noErrs = false
   457  							errTimer.Reset(0)
   458  							log.Warn("blockDetailStats stats report failed", "err", err)
   459  						}
   460  					}
   461  					if url, ok := urlMap["blockAppendTime"]; ok {
   462  						if err := s.sendAppendTimeStats(url, authJwt); err != nil {
   463  							noErrs = false
   464  							errTimer.Reset(0)
   465  							log.Warn("blockAppendTime stats report failed", "err", err)
   466  						}
   467  					}
   468  				}
   469  				errTimer.Reset(0)
   470  			}
   471  			fullReport.Stop()
   472  		}
   473  	}
   474  }
   475  
   476  func (s *Service) initializeURLMap() map[string]string {
   477  	return map[string]string{
   478  		"blockTransactionStats": fmt.Sprintf("http://%s/stats/blockTransactionStats", s.host),
   479  		"blockAppendTime":       fmt.Sprintf("http://%s/stats/blockAppendTime", s.host),
   480  		"blockDetailStats":      fmt.Sprintf("http://%s/stats/blockDetailStats", s.host),
   481  		"nodeStats":             fmt.Sprintf("http://%s/stats/nodeStats", s.host),
   482  		"login":                 fmt.Sprintf("http://%s/auth/login", s.host),
   483  	}
   484  }
   485  
   486  func (s *Service) handleBlock(block *types.Block) {
   487  	// Cache Block
   488  	log.Trace("Handling block", "detailsQueueSize", s.detailStatsQueue.Size(), "appendTimeQueueSize", s.appendTimeStatsQueue.Size(), "transactionQueueSize", s.transactionStatsQueue.Size(), "blockNumber", block.NumberU64())
   489  
   490  	s.cacheBlock(block)
   491  
   492  	if s.sendfullstats {
   493  		dtlStats := s.assembleBlockDetailStats(block)
   494  		if dtlStats != nil {
   495  			s.detailStatsQueue.Enqueue(dtlStats)
   496  		}
   497  	}
   498  
   499  	appStats := s.assembleBlockAppendTimeStats(block)
   500  	if appStats != nil {
   501  		s.appendTimeStatsQueue.Enqueue(appStats)
   502  	}
   503  
   504  	if block.NumberU64()%c_txBatchSize == 0 && s.sendfullstats && block.Header().Location().Context() == common.ZONE_CTX {
   505  		txStats := s.assembleBlockTransactionStats(block)
   506  		if txStats != nil {
   507  			s.transactionStatsQueue.Enqueue(txStats)
   508  		}
   509  	}
   510  
   511  	// After handling a block and potentially adding to the queues, notify the sendStats goroutine
   512  	// that stats are ready to be sent
   513  	s.statsReadyCh <- struct{}{}
   514  }
   515  
   516  func (s *Service) reportNodeStats(url string, mod int, authJwt string) error {
   517  	if url == "" {
   518  		log.Warn("node stats url is empty")
   519  		return errors.New("node stats connection is empty")
   520  	}
   521  
   522  	isRegion := strings.Contains(s.instanceDir, "region")
   523  	isPrime := strings.Contains(s.instanceDir, "prime")
   524  
   525  	if isRegion || isPrime {
   526  		log.Debug("Skipping node stats for region or prime. Filtered out on backend")
   527  		return nil
   528  	}
   529  
   530  	log.Trace("Quai Stats Instance Dir", "path", s.instanceDir+"/../..")
   531  
   532  	// Don't send if dirSize < 1
   533  	// Get disk usage (as a percentage)
   534  	diskUsage, err := dirSize(s.instanceDir + "/../..")
   535  	if err != nil {
   536  		log.Warn("Error calculating directory sizes:", "error", err)
   537  		diskUsage = c_statsErrorValue
   538  	}
   539  
   540  	diskSize, err := diskTotalSize()
   541  	if err != nil {
   542  		log.Warn("Error calculating disk size:", "error", err)
   543  		diskUsage = c_statsErrorValue
   544  	}
   545  
   546  	diskUsagePercent := float64(c_statsErrorValue)
   547  	if diskSize > 0 {
   548  		diskUsagePercent = float64(diskUsage) / float64(diskSize)
   549  	} else {
   550  		log.Warn("Error calculating disk usage percent: disk size is 0")
   551  	}
   552  
   553  	// Usage in your main function
   554  	ramUsage, err := getQuaiRAMUsage()
   555  	if err != nil {
   556  		log.Warn("Error getting Quai RAM usage:", "error", err)
   557  		return err
   558  	}
   559  	var ramUsagePercent, ramFreePercent, ramAvailablePercent float64
   560  	if vmStat, err := mem.VirtualMemory(); err == nil {
   561  		ramUsagePercent = float64(ramUsage) / float64(vmStat.Total)
   562  		ramFreePercent = float64(vmStat.Free) / float64(vmStat.Total)
   563  		ramAvailablePercent = float64(vmStat.Available) / float64(vmStat.Total)
   564  	} else {
   565  		log.Warn("Error getting RAM stats:", "error", err)
   566  		return err
   567  	}
   568  
   569  	// Get CPU usage
   570  	cpuUsageQuai, err := getQuaiCPUUsage()
   571  	if err != nil {
   572  		log.Warn("Error getting Quai CPU percent usage:", "error", err)
   573  		return err
   574  	} else {
   575  		cpuUsageQuai /= float64(100)
   576  	}
   577  
   578  	var cpuFree float32
   579  	if cpuUsageTotal, err := cpu.Percent(0, false); err == nil {
   580  		cpuFree = 1 - float32(cpuUsageTotal[0]/float64(100))
   581  	} else {
   582  		log.Warn("Error getting CPU free:", "error", err)
   583  		return err
   584  	}
   585  
   586  	currentHeader := s.backend.CurrentHeader()
   587  
   588  	if currentHeader == nil {
   589  		log.Warn("Current header is nil")
   590  		return errors.New("current header is nil")
   591  	}
   592  	// Get current block number
   593  	currentBlockHeight := currentHeader.NumberArray()
   594  
   595  	// Get location
   596  	location := currentHeader.Location()
   597  
   598  	// Get the first non-loopback MAC address
   599  	var macAddress string
   600  	interfaces, err := net.Interfaces()
   601  	if err == nil {
   602  		for _, interf := range interfaces {
   603  			if interf.HardwareAddr != nil && len(interf.HardwareAddr.String()) > 0 && (interf.Flags&net.FlagLoopback) == 0 {
   604  				macAddress = interf.HardwareAddr.String()
   605  				break
   606  			}
   607  		}
   608  	} else {
   609  		log.Warn("Error getting MAC address:", err)
   610  		return err
   611  	}
   612  
   613  	// Hash the MAC address
   614  	var hashedMAC string
   615  	if macAddress != "" {
   616  		hash := sha256.Sum256([]byte(macAddress))
   617  		hashedMAC = hex.EncodeToString(hash[:])
   618  	}
   619  
   620  	// Assemble the new node stats
   621  	log.Trace("Sending node details to quaistats")
   622  
   623  	document := map[string]interface{}{
   624  		"id": s.node,
   625  		"nodeStats": &nodeStats{
   626  			Name:                s.node,
   627  			Timestamp:           big.NewInt(time.Now().Unix()), // Current timestamp
   628  			RAMUsage:            int64(ramUsage),
   629  			RAMUsagePercent:     float32(ramUsagePercent),
   630  			RAMFreePercent:      float32(ramFreePercent),
   631  			RAMAvailablePercent: float32(ramAvailablePercent),
   632  			CPUUsagePercent:     float32(cpuUsageQuai),
   633  			CPUFree:             float32(cpuFree),
   634  			DiskUsageValue:      int64(diskUsage),
   635  			DiskUsagePercent:    float32(diskUsagePercent),
   636  			CurrentBlockNumber:  currentBlockHeight,
   637  			RegionLocation:      location.Region(),
   638  			ZoneLocation:        location.Zone(),
   639  			NodeStatsMod:        mod,
   640  			HashedMAC:           hashedMAC,
   641  		},
   642  	}
   643  
   644  	jsonData, err := json.Marshal(document)
   645  	if err != nil {
   646  		log.Error("Failed to marshal node stats", "err", err)
   647  		return err
   648  	}
   649  
   650  	// Create a new HTTP request
   651  	req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonData))
   652  	if err != nil {
   653  		log.Error("Failed to create new HTTP request", "err", err)
   654  		return err
   655  	}
   656  
   657  	// Set headers
   658  	req.Header.Set("Content-Type", "application/json")
   659  	req.Header.Set("Authorization", "Bearer "+authJwt)
   660  
   661  	// Send the request using the default HTTP client
   662  	resp, err := http.DefaultClient.Do(req)
   663  	if err != nil {
   664  		log.Error("Failed to send node stats", "err", err)
   665  		return err
   666  	}
   667  	defer resp.Body.Close()
   668  
   669  	if resp.StatusCode != http.StatusOK {
   670  		body, err := ioutil.ReadAll(resp.Body)
   671  		if err != nil {
   672  			log.Error("Failed to response body", "err", err)
   673  			return err
   674  		}
   675  		log.Error("Received non-OK response", "status", resp.Status, "body", string(body))
   676  		return errors.New("Received non-OK response: " + resp.Status)
   677  	}
   678  	log.Trace("Successfully sent node stats to quaistats")
   679  	return nil
   680  }
   681  
   682  func (s *Service) sendTransactionStats(url string, authJwt string) error {
   683  	if len(s.transactionStatsQueue.data) == 0 {
   684  		return nil
   685  	}
   686  	statsBatch := make([]*blockTransactionStats, 0, c_queueBatchSize)
   687  
   688  	for i := 0; i < int(c_queueBatchSize) && len(s.transactionStatsQueue.data) > 0; i++ {
   689  		stat := s.transactionStatsQueue.Dequeue()
   690  		if stat == nil {
   691  			break
   692  		}
   693  		statsBatch = append(statsBatch, stat.(*blockTransactionStats))
   694  	}
   695  
   696  	if len(statsBatch) == 0 {
   697  		return nil
   698  	}
   699  
   700  	err := s.report(url, "blockTransactionStats", statsBatch, authJwt)
   701  	if err != nil && strings.Contains(err.Error(), "Received non-OK response") {
   702  		log.Warn("Failed to send transaction stats, requeuing stats", "err", err)
   703  		// Re-enqueue the failed stats from end to beginning
   704  		tempSlice := make([]interface{}, len(statsBatch))
   705  		for i, item := range statsBatch {
   706  			tempSlice[len(statsBatch)-1-i] = item
   707  		}
   708  		s.transactionStatsQueue.EnqueueFrontBatch(tempSlice)
   709  		return err
   710  	} else if err != nil {
   711  		log.Warn("Failed to send transaction stats", "err", err)
   712  		return err
   713  	}
   714  	return nil
   715  }
   716  
   717  func (s *Service) sendDetailStats(url string, authJwt string) error {
   718  	if len(s.detailStatsQueue.data) == 0 {
   719  		return nil
   720  	}
   721  	statsBatch := make([]*blockDetailStats, 0, c_queueBatchSize)
   722  
   723  	for i := 0; i < int(c_queueBatchSize) && s.detailStatsQueue.Size() > 0; i++ {
   724  		stat := s.detailStatsQueue.Dequeue()
   725  		if stat == nil {
   726  			break
   727  		}
   728  		statsBatch = append(statsBatch, stat.(*blockDetailStats))
   729  	}
   730  
   731  	if len(statsBatch) == 0 {
   732  		return nil
   733  	}
   734  
   735  	err := s.report(url, "blockDetailStats", statsBatch, authJwt)
   736  	if err != nil && strings.Contains(err.Error(), "Received non-OK response") {
   737  		log.Warn("Failed to send detail stats, requeuing stats", "err", err)
   738  		// Re-enqueue the failed stats from end to beginning
   739  		tempSlice := make([]interface{}, len(statsBatch))
   740  		for i, item := range statsBatch {
   741  			tempSlice[len(statsBatch)-1-i] = item
   742  		}
   743  		s.detailStatsQueue.EnqueueFrontBatch(tempSlice)
   744  		return err
   745  	} else if err != nil {
   746  		log.Warn("Failed to send detail stats", "err", err)
   747  		return err
   748  	}
   749  	return nil
   750  }
   751  
   752  func (s *Service) sendAppendTimeStats(url string, authJwt string) error {
   753  	if len(s.appendTimeStatsQueue.data) == 0 {
   754  		return nil
   755  	}
   756  
   757  	statsBatch := make([]*blockAppendTime, 0, c_queueBatchSize)
   758  
   759  	for i := 0; i < int(c_queueBatchSize) && s.appendTimeStatsQueue.Size() > 0; i++ {
   760  		stat := s.appendTimeStatsQueue.Dequeue()
   761  		if stat == nil {
   762  			break
   763  		}
   764  		statsBatch = append(statsBatch, stat.(*blockAppendTime))
   765  	}
   766  
   767  	if len(statsBatch) == 0 {
   768  		return nil
   769  	}
   770  
   771  	err := s.report(url, "blockAppendTime", statsBatch, authJwt)
   772  	if err != nil && strings.Contains(err.Error(), "Received non-OK response") {
   773  		log.Warn("Failed to send append time stats, requeuing stats", "err", err)
   774  		// Re-enqueue the failed stats from end to beginning
   775  		tempSlice := make([]interface{}, len(statsBatch))
   776  		for i, item := range statsBatch {
   777  			tempSlice[len(statsBatch)-1-i] = item
   778  		}
   779  		s.appendTimeStatsQueue.EnqueueFrontBatch(tempSlice)
   780  		return err
   781  	} else if err != nil {
   782  		log.Warn("Failed to send append time stats", "err", err)
   783  		return err
   784  	}
   785  	return nil
   786  }
   787  
   788  func (s *Service) report(url string, dataType string, stats interface{}, authJwt string) error {
   789  	if url == "" {
   790  		log.Warn(dataType + " url is empty")
   791  		return errors.New(dataType + " url is empty")
   792  	}
   793  
   794  	if stats == nil {
   795  		log.Warn(dataType + " stats are nil")
   796  		return errors.New(dataType + " stats are nil")
   797  	}
   798  
   799  	log.Trace("Sending " + dataType + " stats to quaistats")
   800  
   801  	document := map[string]interface{}{
   802  		"id":     s.node,
   803  		dataType: stats,
   804  	}
   805  
   806  	jsonData, err := json.Marshal(document)
   807  	if err != nil {
   808  		log.Error("Failed to marshal "+dataType+" stats", "err", err)
   809  		return err
   810  	}
   811  
   812  	req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
   813  	if err != nil {
   814  		log.Error("Failed to create new request for "+dataType+" stats", "err", err)
   815  		return err
   816  	}
   817  
   818  	// Add headers
   819  	req.Header.Set("Content-Type", "application/json")
   820  	req.Header.Set("Authorization", "Bearer "+authJwt) // Add this line for the Authorization header
   821  
   822  	client := &http.Client{}
   823  	resp, err := client.Do(req)
   824  	if err != nil {
   825  		log.Error("Failed to send "+dataType+" stats", "err", err)
   826  		return err
   827  	}
   828  	defer resp.Body.Close()
   829  
   830  	if resp.StatusCode != http.StatusOK {
   831  		body, err := ioutil.ReadAll(resp.Body)
   832  		if err != nil {
   833  			log.Error("Failed to response body", "err", err)
   834  			return err
   835  		}
   836  		log.Error("Received non-OK response", "status", resp.Status, "body", string(body))
   837  		return errors.New("Received non-OK response: " + resp.Status)
   838  	}
   839  	log.Trace("Successfully sent " + dataType + " stats to quaistats")
   840  	return nil
   841  }
   842  
   843  type cachedBlock struct {
   844  	number     uint64
   845  	parentHash common.Hash
   846  	txCount    uint64
   847  	time       uint64
   848  }
   849  
   850  // nodeInfo is the collection of meta information about a node that is displayed
   851  // on the monitoring page.
   852  type nodeInfo struct {
   853  	Name     string `json:"name"`
   854  	Node     string `json:"node"`
   855  	Port     int    `json:"port"`
   856  	Network  string `json:"net"`
   857  	Protocol string `json:"protocol"`
   858  	API      string `json:"api"`
   859  	Os       string `json:"os"`
   860  	OsVer    string `json:"os_v"`
   861  	Client   string `json:"client"`
   862  	History  bool   `json:"canUpdateHistory"`
   863  	Chain    string `json:"chain"`
   864  	ChainID  uint64 `json:"chainId"`
   865  }
   866  
   867  // authMsg is the authentication infos needed to login to a monitoring server.
   868  type authMsg struct {
   869  	ID     string      `json:"id"`
   870  	Info   nodeInfo    `json:"info"`
   871  	Secret loginSecret `json:"secret"`
   872  }
   873  
   874  type loginSecret struct {
   875  	Name     string `json:"name"`
   876  	Password string `json:"password"`
   877  }
   878  
   879  type Credentials struct {
   880  	Name     string `json:"name"`
   881  	Password string `json:"password"`
   882  }
   883  
   884  type AuthResponse struct {
   885  	Success bool   `json:"success"`
   886  	Token   string `json:"token"`
   887  }
   888  
   889  func (s *Service) login2(url string) (string, error) {
   890  	// Substitute with your actual service address and port
   891  
   892  	infos := s.server.NodeInfo()
   893  
   894  	var protocols []string
   895  	for _, proto := range s.server.Protocols {
   896  		protocols = append(protocols, fmt.Sprintf("%s/%d", proto.Name, proto.Version))
   897  	}
   898  	var network string
   899  	if info := infos.Protocols["eth"]; info != nil {
   900  		network = fmt.Sprintf("%d", info.(*ethproto.NodeInfo).Network)
   901  	}
   902  
   903  	var secretUser string
   904  	if s.sendfullstats {
   905  		secretUser = "admin"
   906  	} else {
   907  		secretUser = s.node
   908  	}
   909  
   910  	auth := &authMsg{
   911  		ID: s.node,
   912  		Info: nodeInfo{
   913  			Name:     s.node,
   914  			Node:     infos.Name,
   915  			Port:     infos.Ports.Listener,
   916  			Network:  network,
   917  			Protocol: strings.Join(protocols, ", "),
   918  			API:      "No",
   919  			Os:       runtime.GOOS,
   920  			OsVer:    runtime.GOARCH,
   921  			Client:   "0.1.1",
   922  			History:  true,
   923  			Chain:    common.NodeLocation.Name(),
   924  			ChainID:  s.chainID.Uint64(),
   925  		},
   926  		Secret: loginSecret{
   927  			Name:     secretUser,
   928  			Password: s.pass,
   929  		},
   930  	}
   931  
   932  	authJson, err := json.Marshal(auth)
   933  	if err != nil {
   934  		return "", err
   935  	}
   936  
   937  	resp, err := http.Post(url, "application/json", bytes.NewBuffer(authJson))
   938  	if err != nil {
   939  		return "", err
   940  	}
   941  	defer resp.Body.Close()
   942  
   943  	body, err := ioutil.ReadAll(resp.Body)
   944  	if err != nil {
   945  		log.Error("Failed to response body", "err", err)
   946  		return "", err
   947  	}
   948  
   949  	var authResponse AuthResponse
   950  	err = json.Unmarshal(body, &authResponse)
   951  	if err != nil {
   952  		return "", err
   953  	}
   954  
   955  	if authResponse.Success {
   956  		return authResponse.Token, nil
   957  	}
   958  
   959  	return "", fmt.Errorf("login failed")
   960  }
   961  
   962  // isJwtExpired checks if the JWT token is expired
   963  func (s *Service) isJwtExpired(authJwt string) (bool, error) {
   964  	if authJwt == "" {
   965  		return false, errors.New("token is nil")
   966  	}
   967  
   968  	parts := strings.Split(authJwt, ".")
   969  	if len(parts) != 3 {
   970  		return false, errors.New("invalid token")
   971  	}
   972  
   973  	claims := jwt.MapClaims{}
   974  	_, _, err := new(jwt.Parser).ParseUnverified(authJwt, claims)
   975  	if err != nil {
   976  		return false, err
   977  	}
   978  
   979  	if exp, ok := claims["exp"].(float64); ok {
   980  		return time.Now().Unix() >= int64(exp), nil
   981  	}
   982  
   983  	return false, errors.New("exp claim not found in token")
   984  }
   985  
   986  // Trusted Only
   987  type blockTransactionStats struct {
   988  	Timestamp             *big.Int `json:"timestamp"`
   989  	TotalNoTransactions1h uint64   `json:"totalNoTransactions1h"`
   990  	TPS1m                 uint64   `json:"tps1m"`
   991  	TPS1hr                uint64   `json:"tps1hr"`
   992  	Chain                 string   `json:"chain"`
   993  }
   994  
   995  // Trusted Only
   996  type blockDetailStats struct {
   997  	Timestamp    *big.Int `json:"timestamp"`
   998  	ZoneHeight   uint64   `json:"zoneHeight"`
   999  	RegionHeight uint64   `json:"regionHeight"`
  1000  	PrimeHeight  uint64   `json:"primeHeight"`
  1001  	Chain        string   `json:"chain"`
  1002  	Entropy      string   `json:"entropy"`
  1003  	Difficulty   string   `json:"difficulty"`
  1004  }
  1005  
  1006  // Everyone sends every block
  1007  type blockAppendTime struct {
  1008  	AppendTime  time.Duration `json:"appendTime"`
  1009  	BlockNumber *big.Int      `json:"number"`
  1010  	Chain       string        `json:"chain"`
  1011  }
  1012  
  1013  type nodeStats struct {
  1014  	Name                string     `json:"name"`
  1015  	Timestamp           *big.Int   `json:"timestamp"`
  1016  	RAMUsage            int64      `json:"ramUsage"`
  1017  	RAMUsagePercent     float32    `json:"ramUsagePercent"`
  1018  	RAMFreePercent      float32    `json:"ramFreePercent"`
  1019  	RAMAvailablePercent float32    `json:"ramAvailablePercent"`
  1020  	CPUUsagePercent     float32    `json:"cpuPercent"`
  1021  	CPUFree             float32    `json:"cpuFree"`
  1022  	DiskUsagePercent    float32    `json:"diskUsagePercent"`
  1023  	DiskUsageValue      int64      `json:"diskUsageValue"`
  1024  	CurrentBlockNumber  []*big.Int `json:"currentBlockNumber"`
  1025  	RegionLocation      int        `json:"regionLocation"`
  1026  	ZoneLocation        int        `json:"zoneLocation"`
  1027  	NodeStatsMod        int        `json:"nodeStatsMod"`
  1028  	HashedMAC           string     `json:"hashedMAC"`
  1029  }
  1030  
  1031  type tps struct {
  1032  	TPS1m                     uint64
  1033  	TPS1hr                    uint64
  1034  	TotalNumberTransactions1h uint64
  1035  }
  1036  
  1037  type BatchObject struct {
  1038  	TotalNoTransactions uint64
  1039  	OldestBlockTime     uint64
  1040  }
  1041  
  1042  func (s *Service) cacheBlock(block *types.Block) cachedBlock {
  1043  	currentBlock := cachedBlock{
  1044  		number:     block.NumberU64(),
  1045  		parentHash: block.ParentHash(),
  1046  		txCount:    uint64(len(block.Transactions())),
  1047  		time:       block.Time(),
  1048  	}
  1049  	s.blockLookupCache.Add(block.Hash(), currentBlock)
  1050  	return currentBlock
  1051  }
  1052  
  1053  func (s *Service) calculateTPS(block *types.Block) *tps {
  1054  	var totalTransactions1h uint64
  1055  	var totalTransactions1m uint64
  1056  	var currentBlock interface{}
  1057  	var ok bool
  1058  
  1059  	fullNodeBackend := s.backend.(fullNodeBackend)
  1060  	withinMinute := true
  1061  
  1062  	currentBlock, ok = s.blockLookupCache.Get(block.Hash())
  1063  	if !ok {
  1064  		currentBlock = s.cacheBlock(block)
  1065  	}
  1066  
  1067  	for {
  1068  		// If the current block is nil or the block is older than the window size, break
  1069  		if currentBlock == nil || currentBlock.(cachedBlock).time+c_windowSize < block.Time() {
  1070  			break
  1071  		}
  1072  
  1073  		// Add the number of transactions in the block to the total
  1074  		totalTransactions1h += currentBlock.(cachedBlock).txCount
  1075  		if withinMinute && currentBlock.(cachedBlock).time+60 > block.Time() {
  1076  			totalTransactions1m += currentBlock.(cachedBlock).txCount
  1077  		} else {
  1078  			withinMinute = false
  1079  		}
  1080  
  1081  		// If the current block is the genesis block, break
  1082  		if currentBlock.(cachedBlock).number == 1 {
  1083  			break
  1084  		}
  1085  
  1086  		// Get the parent block
  1087  		parentHash := currentBlock.(cachedBlock).parentHash
  1088  		currentBlock, ok = s.blockLookupCache.Get(parentHash)
  1089  		if !ok {
  1090  
  1091  			// If the parent block is not cached, get it from the full node backend and cache it
  1092  			fullBlock, fullBlockOk := fullNodeBackend.BlockByHash(context.Background(), parentHash)
  1093  			if fullBlockOk != nil {
  1094  				log.Error("Error getting block hash", "hash", parentHash.String())
  1095  				return &tps{}
  1096  			}
  1097  			currentBlock = s.cacheBlock(fullBlock)
  1098  		}
  1099  	}
  1100  
  1101  	// Catches if we get to genesis block and are still within the window
  1102  	if currentBlock.(cachedBlock).number == 1 && withinMinute {
  1103  		delta := block.Time() - currentBlock.(cachedBlock).time
  1104  		return &tps{
  1105  			TPS1m:                     totalTransactions1m / delta,
  1106  			TPS1hr:                    totalTransactions1h / delta,
  1107  			TotalNumberTransactions1h: totalTransactions1h,
  1108  		}
  1109  	} else if currentBlock.(cachedBlock).number == 1 {
  1110  		delta := block.Time() - currentBlock.(cachedBlock).time
  1111  		return &tps{
  1112  			TPS1m:                     totalTransactions1m / 60,
  1113  			TPS1hr:                    totalTransactions1h / delta,
  1114  			TotalNumberTransactions1h: totalTransactions1h,
  1115  		}
  1116  	}
  1117  
  1118  	return &tps{
  1119  		TPS1m:                     totalTransactions1m / 60,
  1120  		TPS1hr:                    totalTransactions1h / c_windowSize,
  1121  		TotalNumberTransactions1h: totalTransactions1h,
  1122  	}
  1123  }
  1124  
  1125  func (s *Service) assembleBlockDetailStats(block *types.Block) *blockDetailStats {
  1126  	if block == nil {
  1127  		return nil
  1128  	}
  1129  	header := block.Header()
  1130  	difficulty := header.Difficulty().String()
  1131  
  1132  	// Assemble and return the block stats
  1133  	return &blockDetailStats{
  1134  		Timestamp:    new(big.Int).SetUint64(header.Time()),
  1135  		ZoneHeight:   header.NumberU64(2),
  1136  		RegionHeight: header.NumberU64(1),
  1137  		PrimeHeight:  header.NumberU64(0),
  1138  		Chain:        common.NodeLocation.Name(),
  1139  		Entropy:      common.BigBitsToBits(s.backend.TotalLogS(block.Header())).String(),
  1140  		Difficulty:   difficulty,
  1141  	}
  1142  }
  1143  
  1144  func (s *Service) assembleBlockAppendTimeStats(block *types.Block) *blockAppendTime {
  1145  	if block == nil {
  1146  		return nil
  1147  	}
  1148  	header := block.Header()
  1149  	appendTime := block.GetAppendTime()
  1150  
  1151  	log.Info("Raw Block Append Time", "appendTime", appendTime.Microseconds())
  1152  
  1153  	// Assemble and return the block stats
  1154  	return &blockAppendTime{
  1155  		AppendTime:  appendTime,
  1156  		BlockNumber: header.Number(),
  1157  		Chain:       common.NodeLocation.Name(),
  1158  	}
  1159  }
  1160  
  1161  func (s *Service) assembleBlockTransactionStats(block *types.Block) *blockTransactionStats {
  1162  	if block == nil {
  1163  		return nil
  1164  	}
  1165  	header := block.Header()
  1166  	tps := s.calculateTPS(block)
  1167  	if tps == nil {
  1168  		return nil
  1169  	}
  1170  
  1171  	// Assemble and return the block stats
  1172  	return &blockTransactionStats{
  1173  		Timestamp:             new(big.Int).SetUint64(header.Time()),
  1174  		TotalNoTransactions1h: tps.TotalNumberTransactions1h,
  1175  		TPS1m:                 tps.TPS1m,
  1176  		TPS1hr:                tps.TPS1hr,
  1177  		Chain:                 common.NodeLocation.Name(),
  1178  	}
  1179  }
  1180  
  1181  func getQuaiCPUUsage() (float64, error) {
  1182  	// 'ps' command options might vary depending on your OS
  1183  	cmd := exec.Command("ps", "aux")
  1184  	numCores := runtime.NumCPU()
  1185  
  1186  	output, err := cmd.Output()
  1187  	if err != nil {
  1188  		return 0, err
  1189  	}
  1190  
  1191  	lines := strings.Split(string(output), "\n")
  1192  	var totalCpuUsage float64
  1193  	var cpuUsage float64
  1194  	for _, line := range lines {
  1195  		if strings.Contains(line, "go-quai") {
  1196  			fields := strings.Fields(line)
  1197  			if len(fields) > 2 {
  1198  				// Assuming %CPU is the third column, command is the eleventh
  1199  				cpuUsage, err = strconv.ParseFloat(fields[2], 64)
  1200  				if err != nil {
  1201  					return 0, err
  1202  				}
  1203  				totalCpuUsage += cpuUsage
  1204  			}
  1205  		}
  1206  	}
  1207  
  1208  	if totalCpuUsage == 0 {
  1209  		return 0, errors.New("quai process not found")
  1210  	}
  1211  
  1212  	return totalCpuUsage / float64(numCores), nil
  1213  }
  1214  
  1215  func getQuaiRAMUsage() (uint64, error) {
  1216  	// Get a list of all running processes
  1217  	processes, err := process.Processes()
  1218  	if err != nil {
  1219  		return 0, err
  1220  	}
  1221  
  1222  	var totalRam uint64
  1223  
  1224  	// Debug: log number of processes
  1225  	log.Trace("Number of processes", "number", len(processes))
  1226  
  1227  	for _, p := range processes {
  1228  		cmdline, err := p.Cmdline()
  1229  		if err != nil {
  1230  			// Debug: log error
  1231  			log.Trace("Error getting process cmdline", "error", err)
  1232  			continue
  1233  		}
  1234  
  1235  		if strings.Contains(cmdline, "go-quai") {
  1236  			memInfo, err := p.MemoryInfo()
  1237  			if err != nil {
  1238  				return 0, err
  1239  			}
  1240  			totalRam += memInfo.RSS
  1241  		}
  1242  	}
  1243  
  1244  	if totalRam == 0 {
  1245  		return 0, errors.New("go-quai process not found")
  1246  	}
  1247  
  1248  	return totalRam, nil
  1249  }
  1250  
  1251  // dirSize returns the size of a directory in bytes.
  1252  func dirSize(path string) (int64, error) {
  1253  	var cmd *exec.Cmd
  1254  	if runtime.GOOS == "darwin" {
  1255  		cmd = exec.Command("du", "-sk", path)
  1256  	} else if runtime.GOOS == "linux" {
  1257  		cmd = exec.Command("du", "-bs", path)
  1258  	} else {
  1259  		return -1, errors.New("unsupported OS")
  1260  	}
  1261  	// Execute command
  1262  	output, err := cmd.Output()
  1263  	if err != nil {
  1264  		return -1, err
  1265  	}
  1266  
  1267  	// Split the output and parse the size.
  1268  	sizeStr := strings.Split(string(output), "\t")[0]
  1269  	size, err := strconv.ParseInt(sizeStr, 10, 64)
  1270  	if err != nil {
  1271  		return -1, err
  1272  	}
  1273  
  1274  	// If on macOS, convert size from kilobytes to bytes.
  1275  	if runtime.GOOS == "darwin" {
  1276  		size *= 1024
  1277  	}
  1278  
  1279  	return size, nil
  1280  }
  1281  
  1282  // diskTotalSize returns the total size of the disk in bytes.
  1283  func diskTotalSize() (int64, error) {
  1284  	var cmd *exec.Cmd
  1285  	if runtime.GOOS == "darwin" {
  1286  		cmd = exec.Command("df", "-k", "/")
  1287  	} else if runtime.GOOS == "linux" {
  1288  		cmd = exec.Command("df", "--block-size=1K", "/")
  1289  	} else {
  1290  		return 0, errors.New("unsupported OS")
  1291  	}
  1292  
  1293  	output, err := cmd.Output()
  1294  	if err != nil {
  1295  		return 0, err
  1296  	}
  1297  
  1298  	lines := strings.Split(string(output), "\n")
  1299  	if len(lines) < 2 {
  1300  		return 0, errors.New("unexpected output from df command")
  1301  	}
  1302  
  1303  	fields := strings.Fields(lines[1])
  1304  	if len(fields) < 2 {
  1305  		return 0, errors.New("unexpected output from df command")
  1306  	}
  1307  
  1308  	totalSize, err := strconv.ParseInt(fields[1], 10, 64)
  1309  	if err != nil {
  1310  		return 0, err
  1311  	}
  1312  
  1313  	return totalSize * 1024, nil // convert from kilobytes to bytes
  1314  }