338 lines
6.8 KiB
Go
Executable File
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
|
|
}
|