blockbook/contrib/scripts/check-and-generate-port-reg...

338 lines
6.8 KiB
Go
Executable File

//usr/bin/go run $0 $@ ; exit
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math"
"os"
"path/filepath"
"sort"
"strings"
)
const (
inputDir = "configs/coins"
outputFile = "docs/ports.md"
)
// PortInfo contains backend and blockbook ports
type PortInfo struct {
CoinName string
BlockbookInternalPort uint16
BlockbookPublicPort uint16
BackendRPCPort uint16
BackendServicePorts map[string]uint16
}
// PortInfoSlice is self describing
type PortInfoSlice []*PortInfo
// Config contains coin configuration
type Config struct {
Coin struct {
Name string `json:"name"`
Label string `json:"label"`
}
Ports map[string]uint16 `json:"ports"`
}
func checkPorts() int {
ports := make(map[uint16][]string)
status := 0
files, err := ioutil.ReadDir(inputDir)
if err != nil {
panic(err)
}
for _, fi := range files {
if fi.IsDir() || fi.Name()[0] == '.' {
continue
}
path := filepath.Join(inputDir, fi.Name())
f, err := os.Open(path)
if err != nil {
panic(fmt.Errorf("%s: %s", path, err))
}
defer f.Close()
v := Config{}
d := json.NewDecoder(f)
err = d.Decode(&v)
if err != nil {
panic(fmt.Errorf("%s: json: %s", path, err))
}
if _, ok := v.Ports["blockbook_internal"]; !ok {
fmt.Printf("%s: missing blockbook_internal port\n", v.Coin.Name)
status = 1
}
if _, ok := v.Ports["blockbook_public"]; !ok {
fmt.Printf("%s: missing blockbook_public port\n", v.Coin.Name)
status = 1
}
if _, ok := v.Ports["backend_rpc"]; !ok {
fmt.Printf("%s: missing backend_rpc port\n", v.Coin.Name)
status = 1
}
for _, port := range v.Ports {
if port > 0 {
ports[port] = append(ports[port], v.Coin.Name)
}
}
}
for port, coins := range ports {
if len(coins) > 1 {
fmt.Printf("port %d: registered by %q\n", port, coins)
status = 1
}
}
if status != 0 {
fmt.Println("Got some errors")
}
return status
}
func main() {
output := "stdout"
if len(os.Args) > 1 {
if len(os.Args) == 2 && os.Args[1] == "-w" {
output = outputFile
} else {
fmt.Fprintf(os.Stderr, "Usage: %s [-w]\n", filepath.Base(os.Args[0]))
fmt.Fprintf(os.Stderr, " -w write output to %s instead of stdout\n", outputFile)
os.Exit(1)
}
}
status := checkPorts()
if status != 0 {
os.Exit(status)
}
slice, err := loadPortInfo(inputDir)
if err != nil {
panic(err)
}
sortPortInfo(slice)
err = writeMarkdown(output, slice)
if err != nil {
panic(err)
}
}
func loadPortInfo(dir string) (PortInfoSlice, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
items := make(PortInfoSlice, 0, len(files))
for _, fi := range files {
if fi.IsDir() || fi.Name()[0] == '.' {
continue
}
path := filepath.Join(dir, fi.Name())
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("%s: %s", path, err)
}
defer f.Close()
v := Config{}
d := json.NewDecoder(f)
err = d.Decode(&v)
if err != nil {
return nil, fmt.Errorf("%s: json: %s", path, err)
}
name := v.Coin.Label
if len(name) == 0 {
name = v.Coin.Name
}
item := &PortInfo{CoinName: name, BackendServicePorts: map[string]uint16{}}
for k, v := range v.Ports {
if v == 0 {
continue
}
switch k {
case "blockbook_internal":
item.BlockbookInternalPort = v
case "blockbook_public":
item.BlockbookPublicPort = v
case "backend_rpc":
item.BackendRPCPort = v
default:
if len(k) > 8 && k[:8] == "backend_" {
item.BackendServicePorts[k[8:]] = v
}
}
}
items = append(items, item)
}
return items, nil
}
func sortPortInfo(slice PortInfoSlice) {
// normalizes values in order to sort zero values at the bottom of the slice
normalize := func(a, b uint16) (uint16, uint16) {
if a == 0 {
a = math.MaxUint16
}
if b == 0 {
b = math.MaxUint16
}
return a, b
}
// sort values by BlockbookPublicPort, then by BackendRPCPort and finally by
// CoinName; zero values are sorted at the bottom of the slice
sort.Slice(slice, func(i, j int) bool {
a, b := normalize(slice[i].BlockbookPublicPort, slice[j].BlockbookPublicPort)
if a < b {
return true
}
if a > b {
return false
}
a, b = normalize(slice[i].BackendRPCPort, slice[j].BackendRPCPort)
if a < b {
return true
}
if a > b {
return false
}
return strings.Compare(slice[i].CoinName, slice[j].CoinName) == -1
})
}
func writeMarkdown(output string, slice PortInfoSlice) error {
var (
buf bytes.Buffer
err error
)
fmt.Fprintf(&buf, "# Registry of ports\n\n")
header := []string{"coin", "blockbook internal port", "blockbook public port", "backend rpc port", "backend service ports (zmq)"}
writeTable(&buf, header, slice)
fmt.Fprintf(&buf, "\n> NOTE: This document is generated from coin definitions in `configs/coins`.\n")
out := os.Stdout
if output != "stdout" {
out, err = os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer out.Close()
}
n, err := out.Write(buf.Bytes())
if err != nil {
return err
}
if n < len(buf.Bytes()) {
return io.ErrShortWrite
}
return nil
}
func writeTable(w io.Writer, header []string, slice PortInfoSlice) {
rows := make([][]string, len(slice))
for i, item := range slice {
row := make([]string, len(header))
row[0] = item.CoinName
if item.BlockbookInternalPort > 0 {
row[1] = fmt.Sprintf("%d", item.BlockbookInternalPort)
}
if item.BlockbookPublicPort > 0 {
row[2] = fmt.Sprintf("%d", item.BlockbookPublicPort)
}
if item.BackendRPCPort > 0 {
row[3] = fmt.Sprintf("%d", item.BackendRPCPort)
}
svcPorts := make([]string, 0, len(item.BackendServicePorts))
for k, v := range item.BackendServicePorts {
var s string
if k == "message_queue" {
s = fmt.Sprintf("%d", v)
} else {
s = fmt.Sprintf("%d %s", v, k)
}
svcPorts = append(svcPorts, s)
}
row[4] = strings.Join(svcPorts, ", ")
rows[i] = row
}
padding := make([]int, len(header))
for column := range header {
padding[column] = len(header[column])
for _, row := range rows {
padding[column] = maxInt(padding[column], len(row[column]))
}
}
content := make([][]string, 0, len(rows)+2)
content = append(content, paddedRow(header, padding))
content = append(content, delim("-", padding))
for _, row := range rows {
content = append(content, paddedRow(row, padding))
}
for _, row := range content {
fmt.Fprintf(w, "|%s|\n", strings.Join(row, "|"))
}
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
func paddedRow(row []string, padding []int) []string {
out := make([]string, len(row))
for i := 0; i < len(row); i++ {
format := fmt.Sprintf(" %%-%ds ", padding[i])
out[i] = fmt.Sprintf(format, row[i])
}
return out
}
func delim(str string, padding []int) []string {
out := make([]string, len(padding))
for i := 0; i < len(padding); i++ {
out[i] = strings.Repeat(str, padding[i]+2)
}
return out
}