github.com/cryptohub-digital/blockbook@v0.3.5-0.20240403155730-99ab40b9104c/contrib/scripts/check-and-generate-port-registry.go (about)

     1  // usr/bin/go run $0 $@ ; exit
     2  package main
     3  
     4  import (
     5  	"bytes"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"math"
    11  	"os"
    12  	"path/filepath"
    13  	"sort"
    14  	"strings"
    15  )
    16  
    17  const (
    18  	inputDir   = "configs/coins"
    19  	outputFile = "docs/ports.md"
    20  )
    21  
    22  // PortInfo contains backend and blockbook ports
    23  type PortInfo struct {
    24  	CoinName              string
    25  	BlockbookInternalPort uint16
    26  	BlockbookPublicPort   uint16
    27  	BackendRPCPort        uint16
    28  	BackendServicePorts   map[string]uint16
    29  }
    30  
    31  // PortInfoSlice is self describing
    32  type PortInfoSlice []*PortInfo
    33  
    34  // Config contains coin configuration
    35  type Config struct {
    36  	Coin struct {
    37  		Name  string `json:"name"`
    38  		Label string `json:"label"`
    39  	}
    40  	Ports map[string]uint16 `json:"ports"`
    41  }
    42  
    43  func checkPorts() int {
    44  	ports := make(map[uint16][]string)
    45  	status := 0
    46  
    47  	files, err := ioutil.ReadDir(inputDir)
    48  	if err != nil {
    49  		panic(err)
    50  	}
    51  
    52  	for _, fi := range files {
    53  		if fi.IsDir() || fi.Name()[0] == '.' {
    54  			continue
    55  		}
    56  
    57  		path := filepath.Join(inputDir, fi.Name())
    58  		f, err := os.Open(path)
    59  		if err != nil {
    60  			panic(fmt.Errorf("%s: %s", path, err))
    61  		}
    62  		defer f.Close()
    63  
    64  		v := Config{}
    65  		d := json.NewDecoder(f)
    66  		err = d.Decode(&v)
    67  		if err != nil {
    68  			panic(fmt.Errorf("%s: json: %s", path, err))
    69  		}
    70  
    71  		if _, ok := v.Ports["blockbook_internal"]; !ok {
    72  			fmt.Printf("%s: missing blockbook_internal port\n", v.Coin.Name)
    73  			status = 1
    74  		}
    75  		if _, ok := v.Ports["blockbook_public"]; !ok {
    76  			fmt.Printf("%s: missing blockbook_public port\n", v.Coin.Name)
    77  			status = 1
    78  		}
    79  		if _, ok := v.Ports["backend_rpc"]; !ok {
    80  			fmt.Printf("%s: missing backend_rpc port\n", v.Coin.Name)
    81  			status = 1
    82  		}
    83  
    84  		for _, port := range v.Ports {
    85  			if port > 0 {
    86  				ports[port] = append(ports[port], v.Coin.Name)
    87  			}
    88  		}
    89  	}
    90  
    91  	for port, coins := range ports {
    92  		if len(coins) > 1 {
    93  			fmt.Printf("port %d: registered by %q\n", port, coins)
    94  			status = 1
    95  		}
    96  	}
    97  
    98  	if status != 0 {
    99  		fmt.Println("Got some errors")
   100  	}
   101  	return status
   102  }
   103  
   104  func main() {
   105  	output := "stdout"
   106  	if len(os.Args) > 1 {
   107  		if len(os.Args) == 2 && os.Args[1] == "-w" {
   108  			output = outputFile
   109  		} else {
   110  			fmt.Fprintf(os.Stderr, "Usage: %s [-w]\n", filepath.Base(os.Args[0]))
   111  			fmt.Fprintf(os.Stderr, "    -w    write output to %s instead of stdout\n", outputFile)
   112  			os.Exit(1)
   113  		}
   114  	}
   115  
   116  	status := checkPorts()
   117  	if status != 0 {
   118  		os.Exit(status)
   119  	}
   120  
   121  	slice, err := loadPortInfo(inputDir)
   122  	if err != nil {
   123  		panic(err)
   124  	}
   125  
   126  	sortPortInfo(slice)
   127  
   128  	err = writeMarkdown(output, slice)
   129  	if err != nil {
   130  		panic(err)
   131  	}
   132  }
   133  
   134  func loadPortInfo(dir string) (PortInfoSlice, error) {
   135  	files, err := ioutil.ReadDir(dir)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	items := make(PortInfoSlice, 0, len(files))
   141  
   142  	for _, fi := range files {
   143  		if fi.IsDir() || fi.Name()[0] == '.' {
   144  			continue
   145  		}
   146  
   147  		path := filepath.Join(dir, fi.Name())
   148  		f, err := os.Open(path)
   149  		if err != nil {
   150  			return nil, fmt.Errorf("%s: %s", path, err)
   151  		}
   152  		defer f.Close()
   153  
   154  		v := Config{}
   155  		d := json.NewDecoder(f)
   156  		err = d.Decode(&v)
   157  		if err != nil {
   158  			return nil, fmt.Errorf("%s: json: %s", path, err)
   159  		}
   160  
   161  		name := v.Coin.Label
   162  		if len(name) == 0 {
   163  			name = v.Coin.Name
   164  		}
   165  		item := &PortInfo{CoinName: name, BackendServicePorts: map[string]uint16{}}
   166  		for k, v := range v.Ports {
   167  			if v == 0 {
   168  				continue
   169  			}
   170  
   171  			switch k {
   172  			case "blockbook_internal":
   173  				item.BlockbookInternalPort = v
   174  			case "blockbook_public":
   175  				item.BlockbookPublicPort = v
   176  			case "backend_rpc":
   177  				item.BackendRPCPort = v
   178  			default:
   179  				if len(k) > 8 && k[:8] == "backend_" {
   180  					item.BackendServicePorts[k[8:]] = v
   181  				}
   182  			}
   183  		}
   184  
   185  		items = append(items, item)
   186  	}
   187  
   188  	return items, nil
   189  }
   190  
   191  func sortPortInfo(slice PortInfoSlice) {
   192  	// normalizes values in order to sort zero values at the bottom of the slice
   193  	normalize := func(a, b uint16) (uint16, uint16) {
   194  		if a == 0 {
   195  			a = math.MaxUint16
   196  		}
   197  		if b == 0 {
   198  			b = math.MaxUint16
   199  		}
   200  		return a, b
   201  	}
   202  
   203  	// sort values by BlockbookPublicPort, then by BackendRPCPort and finally by
   204  	// CoinName; zero values are sorted at the bottom of the slice
   205  	sort.Slice(slice, func(i, j int) bool {
   206  		a, b := normalize(slice[i].BlockbookPublicPort, slice[j].BlockbookPublicPort)
   207  
   208  		if a < b {
   209  			return true
   210  		}
   211  		if a > b {
   212  			return false
   213  		}
   214  
   215  		a, b = normalize(slice[i].BackendRPCPort, slice[j].BackendRPCPort)
   216  
   217  		if a < b {
   218  			return true
   219  		}
   220  		if a > b {
   221  			return false
   222  		}
   223  
   224  		return strings.Compare(slice[i].CoinName, slice[j].CoinName) == -1
   225  	})
   226  }
   227  
   228  func writeMarkdown(output string, slice PortInfoSlice) error {
   229  	var (
   230  		buf bytes.Buffer
   231  		err error
   232  	)
   233  
   234  	fmt.Fprintf(&buf, "# Registry of ports\n\n")
   235  
   236  	header := []string{"coin", "blockbook internal port", "blockbook public port", "backend rpc port", "backend service ports (zmq)"}
   237  	writeTable(&buf, header, slice)
   238  
   239  	fmt.Fprintf(&buf, "\n> NOTE: This document is generated from coin definitions in `configs/coins`.\n")
   240  
   241  	out := os.Stdout
   242  	if output != "stdout" {
   243  		out, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
   244  		if err != nil {
   245  			return err
   246  		}
   247  		defer out.Close()
   248  	}
   249  
   250  	n, err := out.Write(buf.Bytes())
   251  	if err != nil {
   252  		return err
   253  	}
   254  	if n < len(buf.Bytes()) {
   255  		return io.ErrShortWrite
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func writeTable(w io.Writer, header []string, slice PortInfoSlice) {
   262  	rows := make([][]string, len(slice))
   263  	for i, item := range slice {
   264  		row := make([]string, len(header))
   265  		row[0] = item.CoinName
   266  		if item.BlockbookInternalPort > 0 {
   267  			row[1] = fmt.Sprintf("%d", item.BlockbookInternalPort)
   268  		}
   269  		if item.BlockbookPublicPort > 0 {
   270  			row[2] = fmt.Sprintf("%d", item.BlockbookPublicPort)
   271  		}
   272  		if item.BackendRPCPort > 0 {
   273  			row[3] = fmt.Sprintf("%d", item.BackendRPCPort)
   274  		}
   275  
   276  		svcPorts := make([]string, 0, len(item.BackendServicePorts))
   277  		for k, v := range item.BackendServicePorts {
   278  			var s string
   279  			if k == "message_queue" {
   280  				s = fmt.Sprintf("%d", v)
   281  			} else {
   282  				s = fmt.Sprintf("%d %s", v, k)
   283  			}
   284  			svcPorts = append(svcPorts, s)
   285  		}
   286  
   287  		row[4] = strings.Join(svcPorts, ", ")
   288  
   289  		rows[i] = row
   290  	}
   291  
   292  	padding := make([]int, len(header))
   293  	for column := range header {
   294  		padding[column] = len(header[column])
   295  
   296  		for _, row := range rows {
   297  			padding[column] = maxInt(padding[column], len(row[column]))
   298  		}
   299  	}
   300  
   301  	content := make([][]string, 0, len(rows)+2)
   302  
   303  	content = append(content, paddedRow(header, padding))
   304  	content = append(content, delim("-", padding))
   305  
   306  	for _, row := range rows {
   307  		content = append(content, paddedRow(row, padding))
   308  	}
   309  
   310  	for _, row := range content {
   311  		fmt.Fprintf(w, "|%s|\n", strings.Join(row, "|"))
   312  	}
   313  }
   314  
   315  func maxInt(a, b int) int {
   316  	if a > b {
   317  		return a
   318  	}
   319  	return b
   320  }
   321  
   322  func paddedRow(row []string, padding []int) []string {
   323  	out := make([]string, len(row))
   324  	for i := 0; i < len(row); i++ {
   325  		format := fmt.Sprintf(" %%-%ds ", padding[i])
   326  		out[i] = fmt.Sprintf(format, row[i])
   327  	}
   328  	return out
   329  }
   330  
   331  func delim(str string, padding []int) []string {
   332  	out := make([]string, len(padding))
   333  	for i := 0; i < len(padding); i++ {
   334  		out[i] = strings.Repeat(str, padding[i]+2)
   335  	}
   336  	return out
   337  }