mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-08-28 14:40:51 +02:00
lncli: Add graphical output of routing table
LIGHT-131, LIGHT-140, LIGHT-138 `lncli showroutingtable` may output routing table as image. Use graphviz for graph rendering. Add explicit version dependency for tools. Add error checking.
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
@@ -14,8 +16,10 @@ import (
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/net/context"
|
||||
"github.com/BitfuryLightning/tools/rt"
|
||||
"github.com/BitfuryLightning/tools/rt/graph/prefix_tree"
|
||||
"github.com/BitfuryLightning/tools/prefix_tree"
|
||||
"github.com/BitfuryLightning/tools/rt/graph"
|
||||
|
||||
"github.com/BitfuryLightning/tools/rt/visualizer"
|
||||
)
|
||||
|
||||
// TODO(roasbeef): cli logic for supporting both positional and unix style
|
||||
@@ -528,54 +532,212 @@ func sendPaymentCommand(ctx *cli.Context) error {
|
||||
var ShowRoutingTableCommand = cli.Command{
|
||||
Name: "showroutingtable",
|
||||
Description: "shows routing table for a node",
|
||||
Usage: "showroutingtable [--table]",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "table",
|
||||
Usage: "Show the routing table in table format. Print only a few first symbols of id",
|
||||
Usage: "showroutingtable text|image",
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "text",
|
||||
Usage: "[--table|--human]",
|
||||
Description: "Show routing table in textual format. By default in JSON",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "table",
|
||||
Usage: "Print channels in routing table in table format.",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "human",
|
||||
Usage: "Print channels in routing table in table format. Output lightning_id partially - only a few first symbols which uniquelly identifies it.",
|
||||
},
|
||||
},
|
||||
Action: showRoutingTableAsText,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "human",
|
||||
Usage: "Simplify output to human readable form. Output lightning_id partially. Only work with --table option.",
|
||||
{
|
||||
Name: "image",
|
||||
Usage: "[--type <IMAGE_TYPE>] [--dest OUTPUT_FILE] [--open]",
|
||||
Description: "Create image with graphical representation of routing table",
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "type",
|
||||
Usage: "Type of image file. Use one of: http://www.graphviz.org/content/output-formats. Usage of this option supresses textual output",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dest",
|
||||
Usage: "Specifies where to save the generated file. If don't specified use os.TempDir Usage of this option supresses textual output",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "open",
|
||||
Usage: "Open generated file automatically. Uses command line \"open\" command",
|
||||
},
|
||||
},
|
||||
Action: showRoutingTableAsImage,
|
||||
},
|
||||
},
|
||||
|
||||
Action: showRoutingTable,
|
||||
}
|
||||
|
||||
func showRoutingTable(ctx *cli.Context) error {
|
||||
ctxb := context.Background()
|
||||
client := getClient(ctx)
|
||||
|
||||
func getRoutingTable(ctxb context.Context, client lnrpc.LightningClient) (*rt.RoutingTable, error) {
|
||||
req := &lnrpc.ShowRoutingTableRequest{}
|
||||
resp, err := client.ShowRoutingTable(ctxb, req)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
// TODO(mkl): maybe it is better to print output directly omitting
|
||||
// conversion to RoutingTable. This part is not performance critical so
|
||||
// I think it is ok because it enables code reuse
|
||||
|
||||
r := rt.NewRoutingTable()
|
||||
for _, channel := range resp.Channels {
|
||||
r.AddChannel(
|
||||
graph.NewID(channel.Id1),
|
||||
graph.NewID(channel.Id2),
|
||||
graph.NewEdgeID(channel.EdgeID),
|
||||
graph.NewEdgeID(channel.Outpoint),
|
||||
&rt.ChannelInfo{channel.Capacity, channel.Weight},
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("Can't unmarshall routing table")
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func showRoutingTableAsText(ctx *cli.Context) error {
|
||||
ctxb := context.Background()
|
||||
client := getClient(ctx)
|
||||
|
||||
r, err := getRoutingTable(ctxb, client)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Bool("table") && ctx.Bool("human"){
|
||||
return fmt.Errorf("--table and --human cannot be used at the same time")
|
||||
}
|
||||
|
||||
if ctx.Bool("table") {
|
||||
printRTAsTable(r, ctx.Bool("human"))
|
||||
printRTAsTable(r, false)
|
||||
} else if ctx.Bool("human") {
|
||||
printRTAsTable(r, true)
|
||||
} else {
|
||||
printRTAsJSON(r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func showRoutingTableAsImage(ctx *cli.Context) error {
|
||||
ctxb := context.Background()
|
||||
client := getClient(ctx)
|
||||
|
||||
r, err := getRoutingTable(ctxb, client)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
reqGetInfo := &lnrpc.GetInfoRequest{}
|
||||
respGetInfo, err := client.GetInfo(ctxb, reqGetInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
selfLightningId, err := hex.DecodeString(respGetInfo.LightningId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
imgType := ctx.String("type")
|
||||
imgDest := ctx.String("dest")
|
||||
if imgType == "" && imgDest == "" {
|
||||
return fmt.Errorf("One or both of --type or --dest should be specified")
|
||||
}
|
||||
|
||||
tempFile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var imageFile *os.File
|
||||
// if the type is not specified explicitly parse the filename
|
||||
if imgType == "" {
|
||||
imgType = filepath.Ext(imgDest)[1:]
|
||||
}
|
||||
// if the filename is not specified explicitly use tempfile
|
||||
if imgDest == "" {
|
||||
imageFile, err = TempFileWithSuffix("", "rt_", "."+ imgType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
imageFile, err = os.Create(imgDest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, ok := visualizer.SupportedFormatsAsMap()[imgType]; !ok {
|
||||
fmt.Printf("Format: '%v' not recognized. Use one of: %v\n", imgType, visualizer.SupportedFormats())
|
||||
return nil
|
||||
}
|
||||
// generate description graph by dot language
|
||||
err = writeToTempFile(r, tempFile, selfLightningId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = writeToImageFile(tempFile, imageFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.Bool("open") {
|
||||
if err := visualizer.Open(imageFile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeToTempFile(r *rt.RoutingTable, file *os.File, self []byte) error {
|
||||
slc := []graph.ID{graph.NewID(string(self))}
|
||||
viz := visualizer.New(r.G, slc, nil, nil)
|
||||
viz.ApplyToNode = func(s string) string { return hex.EncodeToString([]byte(s)) }
|
||||
viz.ApplyToEdge = func(info interface{}) string {
|
||||
if info, ok := info.(*rt.ChannelInfo); ok {
|
||||
return fmt.Sprintf(`"%v"`, info.Capacity())
|
||||
}
|
||||
return "nil"
|
||||
}
|
||||
// need to call method if plan to use shortcut, autocomplete, etc
|
||||
viz.BuildPrefixTree()
|
||||
viz.EnableShortcut(true)
|
||||
dot := viz.Draw()
|
||||
_, err := file.Write([]byte(dot))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = file.Sync()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeToImageFile(TempFile, ImageFile *os.File) error {
|
||||
err := visualizer.Run("neato", TempFile, ImageFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = TempFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Remove(TempFile.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ImageFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// get around a bug in the standard library, add suffix param
|
||||
func TempFileWithSuffix(dir, prefix, suffix string) (*os.File, error) {
|
||||
f, err := ioutil.TempFile(dir, prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f.Name())
|
||||
f, err = os.Create(f.Name()+suffix)
|
||||
return f, err
|
||||
}
|
||||
|
||||
// Prints routing table in human readable table format
|
||||
func printRTAsTable(r *rt.RoutingTable, humanForm bool) {
|
||||
// Minimum length of data part to which name can be shortened
|
||||
@@ -589,7 +751,7 @@ func printRTAsTable(r *rt.RoutingTable, humanForm bool) {
|
||||
tmpl = "%-64v %-64v %-66v %-10v %-10v\n"
|
||||
minLen = 100
|
||||
}
|
||||
fmt.Printf(tmpl, "ID1", "ID2", "EdgeID", "Capacity", "Weight")
|
||||
fmt.Printf(tmpl, "ID1", "ID2", "Outpoint", "Capacity", "Weight")
|
||||
channels := r.AllChannels()
|
||||
if humanForm {
|
||||
// Generate prefix tree for shortcuts
|
||||
@@ -639,8 +801,8 @@ func printRTAsJSON(r *rt.RoutingTable) {
|
||||
type ChannelDesc struct {
|
||||
ID1 string `json:"lightning_id1"`
|
||||
ID2 string `json:"lightning_id2"`
|
||||
EdgeId string `json:"edge_id"`
|
||||
Capacity float64 `json:"capacity"`
|
||||
EdgeId string `json:"outpoint"`
|
||||
Capacity int64 `json:"capacity"`
|
||||
Weight float64 `json:"weight"`
|
||||
}
|
||||
var channels struct {
|
||||
|
Reference in New Issue
Block a user