storageminer: Improve CLI UX

This commit is contained in:
Łukasz Magiera 2020-05-01 14:06:19 +02:00
parent b535a801bc
commit d9acd484c0
6 changed files with 196 additions and 89 deletions

View File

@ -98,6 +98,16 @@ func SectorSetSizes(ctx context.Context, sm *StateManager, maddr address.Address
return api.MinerSectors{}, xerrors.Errorf("(get sset) failed to load miner actor state: %w", err) return api.MinerSectors{}, xerrors.Errorf("(get sset) failed to load miner actor state: %w", err)
} }
notProving, err := abi.BitFieldUnion(mas.Faults, mas.Recoveries)
if err != nil {
return api.MinerSectors{}, err
}
npc, err := notProving.Count()
if err != nil {
return api.MinerSectors{}, err
}
blks := cbor.NewCborStore(sm.ChainStore().Blockstore()) blks := cbor.NewCborStore(sm.ChainStore().Blockstore())
ss, err := amt.LoadAMT(ctx, blks, mas.Sectors) ss, err := amt.LoadAMT(ctx, blks, mas.Sectors)
if err != nil { if err != nil {
@ -106,6 +116,7 @@ func SectorSetSizes(ctx context.Context, sm *StateManager, maddr address.Address
return api.MinerSectors{ return api.MinerSectors{
Sset: ss.Count, Sset: ss.Count,
Pset: ss.Count - npc,
}, nil }, nil
} }

View File

@ -79,18 +79,18 @@ func SizeStr(bi BigInt) string {
return fmt.Sprintf("%.3g %s", f, byteSizeUnits[i]) return fmt.Sprintf("%.3g %s", f, byteSizeUnits[i])
} }
var decUnits = []string{"", "K", "M", "G", "T", "P", "E", "Z"} var deciUnits = []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"}
func DecStr(bi BigInt) string { func DeciStr(bi BigInt) string {
r := new(big.Rat).SetInt(bi.Int) r := new(big.Rat).SetInt(bi.Int)
den := big.NewRat(1, 1000) den := big.NewRat(1, 1024)
var i int var i int
for f, _ := r.Float64(); f >= 1000 && i+1 < len(decUnits); f, _ = r.Float64() { for f, _ := r.Float64(); f >= 1024 && i+1 < len(deciUnits); f, _ = r.Float64() {
i++ i++
r = r.Mul(r, den) r = r.Mul(r, den)
} }
f, _ := r.Float64() f, _ := r.Float64()
return fmt.Sprintf("%.3g %s", f, decUnits[i]) return fmt.Sprintf("%.3g %s", f, deciUnits[i])
} }

View File

@ -4,8 +4,10 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"golang.org/x/xerrors" "sort"
"github.com/fatih/color"
"golang.org/x/xerrors"
"gopkg.in/urfave/cli.v2" "gopkg.in/urfave/cli.v2"
"github.com/filecoin-project/specs-actors/actors/builtin/miner" "github.com/filecoin-project/specs-actors/actors/builtin/miner"
@ -19,7 +21,12 @@ import (
var infoCmd = &cli.Command{ var infoCmd = &cli.Command{
Name: "info", Name: "info",
Usage: "Print storage miner info", Usage: "Print storage miner info",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "color"},
},
Action: func(cctx *cli.Context) error { Action: func(cctx *cli.Context) error {
color.NoColor = !cctx.Bool("color")
nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx) nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
if err != nil { if err != nil {
return err return err
@ -54,7 +61,7 @@ var infoCmd = &cli.Command{
} }
} }
fmt.Printf("Miner: %s\n", maddr) fmt.Printf("Miner: %s\n", color.BlueString("%s", maddr))
// Sector size // Sector size
mi, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK) mi, err := api.StateMinerInfo(ctx, maddr, types.EmptyTSK)
@ -71,8 +78,17 @@ var infoCmd = &cli.Command{
rpercI := types.BigDiv(types.BigMul(pow.MinerPower.RawBytePower, types.NewInt(1000000)), pow.TotalPower.RawBytePower) rpercI := types.BigDiv(types.BigMul(pow.MinerPower.RawBytePower, types.NewInt(1000000)), pow.TotalPower.RawBytePower)
qpercI := types.BigDiv(types.BigMul(pow.MinerPower.QualityAdjPower, types.NewInt(1000000)), pow.TotalPower.QualityAdjPower) qpercI := types.BigDiv(types.BigMul(pow.MinerPower.QualityAdjPower, types.NewInt(1000000)), pow.TotalPower.QualityAdjPower)
fmt.Printf("Byte Power: %s / %s (%0.4f%%)\n", types.SizeStr(pow.MinerPower.RawBytePower), types.SizeStr(pow.TotalPower.RawBytePower), float64(rpercI.Int64())/10000)
fmt.Printf("Actual Power: %s / %s (%0.4f%%)\n", types.DecStr(pow.MinerPower.QualityAdjPower), types.DecStr(pow.TotalPower.QualityAdjPower), float64(qpercI.Int64())/10000) fmt.Printf("Byte Power: %s / %s (%0.4f%%)\n",
color.BlueString(types.SizeStr(pow.MinerPower.RawBytePower)),
types.SizeStr(pow.TotalPower.RawBytePower),
float64(rpercI.Int64())/10000)
fmt.Printf("Actual Power: %s / %s (%0.4f%%)\n",
color.GreenString(types.DeciStr(pow.MinerPower.QualityAdjPower)),
types.DeciStr(pow.TotalPower.QualityAdjPower),
float64(qpercI.Int64())/10000)
secCounts, err := api.StateMinerSectorCount(ctx, maddr, types.EmptyTSK) secCounts, err := api.StateMinerSectorCount(ctx, maddr, types.EmptyTSK)
if err != nil { if err != nil {
return err return err
@ -92,15 +108,17 @@ var infoCmd = &cli.Command{
float64(10000*uint64(len(faults))/secCounts.Pset)/100.) float64(10000*uint64(len(faults))/secCounts.Pset)/100.)
} }
fmt.Printf("Miner Balance: %s\n", types.FIL(mact.Balance)) fmt.Println()
fmt.Printf("Miner Balance: %s\n", color.YellowString("%s", types.FIL(mact.Balance)))
fmt.Printf("\tPreCommit: %s\n", types.FIL(mas.PreCommitDeposits)) fmt.Printf("\tPreCommit: %s\n", types.FIL(mas.PreCommitDeposits))
fmt.Printf("\tLocked: %s\n", types.FIL(mas.LockedFunds)) fmt.Printf("\tLocked: %s\n", types.FIL(mas.LockedFunds))
fmt.Printf("\tAvailable: %s\n", types.FIL(types.BigSub(mact.Balance, types.BigAdd(mas.LockedFunds, mas.PreCommitDeposits)))) color.Green("\tAvailable: %s", types.FIL(types.BigSub(mact.Balance, types.BigAdd(mas.LockedFunds, mas.PreCommitDeposits))))
wb, err := api.WalletBalance(ctx, mi.Worker) wb, err := api.WalletBalance(ctx, mi.Worker)
if err != nil { if err != nil {
return xerrors.Errorf("getting worker balance: %w", err) return xerrors.Errorf("getting worker balance: %w", err)
} }
fmt.Printf("Worker Balance: %s\n", types.FIL(wb)) color.Cyan("Worker Balance: %s", types.FIL(wb))
mb, err := api.StateMarketBalance(ctx, maddr, types.EmptyTSK) mb, err := api.StateMarketBalance(ctx, maddr, types.EmptyTSK)
if err != nil { if err != nil {
@ -109,53 +127,14 @@ var infoCmd = &cli.Command{
fmt.Printf("Market (Escrow): %s\n", types.FIL(mb.Escrow)) fmt.Printf("Market (Escrow): %s\n", types.FIL(mb.Escrow))
fmt.Printf("Market (Locked): %s\n", types.FIL(mb.Locked)) fmt.Printf("Market (Locked): %s\n", types.FIL(mb.Locked))
/*// TODO: indicate whether the post worker is in use fmt.Println()
wstat, err := nodeApi.WorkerStats(ctx)
fmt.Println("Sectors:")
err = sectorsInfo(ctx, nodeApi)
if err != nil { if err != nil {
return err return err
} }
fmt.Printf("Worker use:\n")
fmt.Printf("\tLocal: %d / %d (+%d reserved)\n", wstat.LocalTotal-wstat.LocalReserved-wstat.LocalFree, wstat.LocalTotal-wstat.LocalReserved, wstat.LocalReserved)
fmt.Printf("\tRemote: %d / %d\n", wstat.RemotesTotal-wstat.RemotesFree, wstat.RemotesTotal)
fmt.Printf("Queues:\n")
fmt.Printf("\tAddPiece: %d\n", wstat.AddPieceWait)
fmt.Printf("\tPreCommit: %d\n", wstat.PreCommitWait)
fmt.Printf("\tCommit: %d\n", wstat.CommitWait)
fmt.Printf("\tUnseal: %d\n", wstat.UnsealWait)*/
/*ps, err := api.StateMinerPostState(ctx, maddr, types.EmptyTSK)
if err != nil {
return err
}
if ps.ProvingPeriodStart != 0 {
head, err := api.ChainHead(ctx)
if err != nil {
return err
}
fallback := ps.ProvingPeriodStart - head.Height()
fallbackS := fallback * build.BlockDelay
next := fallback + power.WindowedPostChallengeDuration
nextS := next * build.BlockDelay
fmt.Printf("PoSt Submissions:\n")
fmt.Printf("\tFallback: Epoch %d (in %d blocks, ~%dm %ds)\n", ps.ProvingPeriodStart, fallback, fallbackS/60, fallbackS%60)
fmt.Printf("\tDeadline: Epoch %d (in %d blocks, ~%dm %ds)\n", ps.ProvingPeriodStart+build.SlashablePowerDelay, next, nextS/60, nextS%60)
fmt.Printf("\tConsecutive Failures: %d\n", ps.NumConsecutiveFailures)
} else {
fmt.Printf("Proving Period: Not Proving\n")
}*/
sinfo, err := sectorsInfo(ctx, nodeApi)
if err != nil {
return err
}
fmt.Println("Sectors: ", sinfo)
// TODO: grab actr state / info // TODO: grab actr state / info
// * Sealed sectors (count / bytes) // * Sealed sectors (count / bytes)
// * Power // * Power
@ -163,23 +142,78 @@ var infoCmd = &cli.Command{
}, },
} }
func sectorsInfo(ctx context.Context, napi api.StorageMiner) (map[sealing.SectorState]int, error) { type stateMeta struct{
i int
col color.Attribute
state sealing.SectorState
}
var stateOrder = map[sealing.SectorState]stateMeta{}
var stateList = []stateMeta{
{col: 39, state: "Total"},
{col: color.FgGreen, state: sealing.Proving},
{col: color.FgRed, state: sealing.UndefinedSectorState},
{col: color.FgYellow, state: sealing.Empty},
{col: color.FgYellow, state: sealing.Packing},
{col: color.FgYellow, state: sealing.PreCommit1},
{col: color.FgYellow, state: sealing.PreCommit2},
{col: color.FgYellow, state: sealing.PreCommitting},
{col: color.FgYellow, state: sealing.WaitSeed},
{col: color.FgYellow, state: sealing.Committing},
{col: color.FgYellow, state: sealing.CommitWait},
{col: color.FgYellow, state: sealing.FinalizeSector},
{col: color.FgRed, state: sealing.FailedUnrecoverable},
{col: color.FgRed, state: sealing.SealFailed},
{col: color.FgRed, state: sealing.PreCommitFailed},
{col: color.FgRed, state: sealing.ComputeProofFailed},
{col: color.FgRed, state: sealing.CommitFailed},
{col: color.FgRed, state: sealing.PackingFailed},
{col: color.FgRed, state: sealing.Faulty},
{col: color.FgRed, state: sealing.FaultReported},
{col: color.FgRed, state: sealing.FaultedFinal},
}
func init() {
for i, state := range stateList {
stateOrder[state.state] = stateMeta{
i: i,
col: state.col,
}
}
}
func sectorsInfo(ctx context.Context, napi api.StorageMiner) error {
sectors, err := napi.SectorsList(ctx) sectors, err := napi.SectorsList(ctx)
if err != nil { if err != nil {
return nil, err return err
} }
out := map[sealing.SectorState]int{ buckets := map[sealing.SectorState]int{
"Total": len(sectors), "Total": len(sectors),
} }
for _, s := range sectors { for _, s := range sectors {
st, err := napi.SectorsStatus(ctx, s) st, err := napi.SectorsStatus(ctx, s)
if err != nil { if err != nil {
return nil, err return err
} }
out[sealing.SectorState(st.State)]++ buckets[sealing.SectorState(st.State)]++
} }
return out, nil var sorted []stateMeta
for state, i := range buckets {
sorted = append(sorted, stateMeta{i: i, state: state})
}
sort.Slice(sorted, func(i, j int) bool {
return stateOrder[sorted[i].state].i < stateOrder[sorted[j].state].i
})
for _, s := range sorted {
_, _ = color.New(stateOrder[s.state].col).Printf("\t%s: %d\n", s.state, s.i)
}
return nil
} }

View File

@ -8,8 +8,10 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/fatih/color"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
"golang.org/x/xerrors" "golang.org/x/xerrors"
@ -117,7 +119,12 @@ var storageAttachCmd = &cli.Command{
var storageListCmd = &cli.Command{ var storageListCmd = &cli.Command{
Name: "list", Name: "list",
Usage: "list local storage paths", Usage: "list local storage paths",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "color"},
},
Action: func(cctx *cli.Context) error { Action: func(cctx *cli.Context) error {
color.NoColor = !cctx.Bool("color")
nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx) nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
if err != nil { if err != nil {
return err return err
@ -138,15 +145,25 @@ var storageListCmd = &cli.Command{
sorted := make([]struct { sorted := make([]struct {
stores.ID stores.ID
sectors []stores.Decl sectors []stores.Decl
stat stores.FsStat
}, 0, len(st)) }, 0, len(st))
for id, decls := range st { for id, decls := range st {
st, err := nodeApi.StorageStat(ctx, id)
if err != nil {
return err
}
sorted = append(sorted, struct { sorted = append(sorted, struct {
stores.ID stores.ID
sectors []stores.Decl sectors []stores.Decl
}{id, decls}) stat stores.FsStat
}{id, decls, st})
} }
sort.Slice(sorted, func(i, j int) bool { sort.Slice(sorted, func(i, j int) bool {
if sorted[i].stat.Capacity != sorted[j].stat.Capacity {
return sorted[i].stat.Capacity > sorted[j].stat.Capacity
}
return sorted[i].ID < sorted[j].ID return sorted[i].ID < sorted[j].ID
}) })
@ -168,13 +185,29 @@ var storageListCmd = &cli.Command{
} }
ping := time.Now().Sub(pingStart) ping := time.Now().Sub(pingStart)
usedPercent := (st.Capacity-st.Available)*100/st.Capacity
percCol := color.FgGreen
switch {
case usedPercent > 98:
percCol = color.FgRed
case usedPercent > 90:
percCol = color.FgYellow
}
var barCols = uint64(50)
set := (st.Capacity-st.Available)*barCols/st.Capacity
bar := strings.Repeat("|", int(set)) + strings.Repeat(" ", int(barCols-set))
fmt.Printf("%s:\n", s.ID) fmt.Printf("%s:\n", s.ID)
fmt.Printf("\tUnsealed: %d; Sealed: %d; Caches: %d\n", cnt[0], cnt[1], cnt[2]) fmt.Printf("\t[%s] %s/%s %s\n", color.New(percCol).Sprint(bar),
fmt.Printf("\tSpace Used: %s/%s %d%% (%s avail)\n",
types.SizeStr(types.NewInt(st.Capacity-st.Available)), types.SizeStr(types.NewInt(st.Capacity-st.Available)),
types.SizeStr(types.NewInt(st.Capacity)), types.SizeStr(types.NewInt(st.Capacity)),
(st.Capacity-st.Available)*100/st.Capacity, color.New(percCol).Sprintf("%d%%", usedPercent))
types.SizeStr(types.NewInt(st.Available))) fmt.Printf("\t%s; %s; %s\n",
color.YellowString("Unsealed: %d", cnt[0]),
color.GreenString("Sealed: %d", cnt[1]),
color.BlueString("Caches: %d", cnt[2]))
si, err := nodeApi.StorageInfo(ctx, s.ID) si, err := nodeApi.StorageInfo(ctx, s.ID)
if err != nil { if err != nil {
@ -185,18 +218,18 @@ var storageListCmd = &cli.Command{
if si.CanSeal || si.CanStore { if si.CanSeal || si.CanStore {
fmt.Printf("Weight: %d; Use: ", si.Weight) fmt.Printf("Weight: %d; Use: ", si.Weight)
if si.CanSeal { if si.CanSeal {
fmt.Print("Seal ") fmt.Print(color.MagentaString("Seal "))
} }
if si.CanStore { if si.CanStore {
fmt.Print("Store") fmt.Print(color.CyanString("Store"))
} }
fmt.Println("") fmt.Println("")
} else { } else {
fmt.Println("Use: ReadOnly") fmt.Print(color.HiYellowString("Use: ReadOnly"))
} }
if localPath, ok := local[s.ID]; ok { if localPath, ok := local[s.ID]; ok {
fmt.Printf("\tLocal: %s\n", localPath) fmt.Printf("\tLocal: %s\n", color.GreenString(localPath))
} }
for i, l := range si.URLs { for i, l := range si.URLs {
var rtt string var rtt string
@ -206,6 +239,7 @@ var storageListCmd = &cli.Command{
fmt.Printf("\tURL: %s%s\n", l, rtt) // TODO; try pinging maybe?? print latency? fmt.Printf("\tURL: %s%s\n", l, rtt) // TODO; try pinging maybe?? print latency?
} }
fmt.Println()
} }
return nil return nil

View File

@ -2,11 +2,15 @@ package main
import ( import (
"fmt" "fmt"
"github.com/filecoin-project/lotus/chain/types"
"github.com/filecoin-project/sector-storage/storiface"
"gopkg.in/urfave/cli.v2"
"sort" "sort"
"strings"
"github.com/fatih/color"
"gopkg.in/urfave/cli.v2"
"github.com/filecoin-project/sector-storage/storiface"
"github.com/filecoin-project/lotus/chain/types"
lcli "github.com/filecoin-project/lotus/cli" lcli "github.com/filecoin-project/lotus/cli"
) )
@ -21,7 +25,12 @@ var workersCmd = &cli.Command{
var workersListCmd = &cli.Command{ var workersListCmd = &cli.Command{
Name: "list", Name: "list",
Usage: "list workers", Usage: "list workers",
Flags: []cli.Flag{
&cli.BoolFlag{Name: "color"},
},
Action: func(cctx *cli.Context) error { Action: func(cctx *cli.Context) error {
color.NoColor = !cctx.Bool("color")
nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx) nodeApi, closer, err := lcli.GetStorageMinerAPI(cctx)
if err != nil { if err != nil {
return err return err
@ -51,30 +60,48 @@ var workersListCmd = &cli.Command{
for _, stat := range st { for _, stat := range st {
gpuUse := "not " gpuUse := "not "
gpuCol := color.FgBlue
if stat.GpuUsed { if stat.GpuUsed {
gpuCol = color.FgGreen
gpuUse = "" gpuUse = ""
} }
fmt.Printf("Worker %d, host %s\n", stat.id, stat.Info.Hostname)
fmt.Printf("\tCPU: %d core(s) in use\n", stat.CpuUse) fmt.Printf("Worker %d, host %s\n", stat.id, color.MagentaString(stat.Info.Hostname))
var barCols = uint64(64)
cpuBars := int(stat.CpuUse * barCols / stat.Info.Resources.CPUs)
cpuBar := strings.Repeat("|", cpuBars) + strings.Repeat(" ", int(barCols) - cpuBars)
fmt.Printf("\tCPU: [%s] %d core(s) in use\n", color.GreenString(cpuBar), stat.CpuUse)
ramBarsRes := int(stat.Info.Resources.MemReserved*barCols/stat.Info.Resources.MemPhysical)
ramBarsUsed := int(stat.MemUsedMin*barCols/stat.Info.Resources.MemPhysical)
ramBar := color.YellowString(strings.Repeat("|", ramBarsRes)) +
color.GreenString(strings.Repeat("|", ramBarsUsed)) +
strings.Repeat(" ", int(barCols) - ramBarsUsed - ramBarsRes)
vmem := stat.Info.Resources.MemPhysical+stat.Info.Resources.MemSwap
vmemBarsRes := int(stat.Info.Resources.MemReserved*barCols/vmem)
vmemBarsUsed := int(stat.MemUsedMax*barCols/vmem)
vmemBar := color.YellowString(strings.Repeat("|", vmemBarsRes)) +
color.GreenString(strings.Repeat("|", vmemBarsUsed)) +
strings.Repeat(" ", int(barCols) - vmemBarsUsed - vmemBarsRes)
fmt.Printf("\tRAM: [%s] %d%% %s/%s\n", ramBar,
(stat.Info.Resources.MemReserved + stat.MemUsedMin)*100/stat.Info.Resources.MemPhysical,
types.SizeStr(types.NewInt(stat.Info.Resources.MemReserved + stat.MemUsedMin)),
types.SizeStr(types.NewInt(stat.Info.Resources.MemPhysical)))
fmt.Printf("\tVMEM: [%s] %d%% %s/%s\n", vmemBar,
(stat.Info.Resources.MemReserved + stat.MemUsedMax)*100/vmem,
types.SizeStr(types.NewInt(stat.Info.Resources.MemReserved + stat.MemUsedMax)),
types.SizeStr(types.NewInt(vmem)))
for _, gpu := range stat.Info.Resources.GPUs { for _, gpu := range stat.Info.Resources.GPUs {
fmt.Printf("\tGPU: %s, %sused\n", gpu, gpuUse) fmt.Printf("\tGPU: %s\n", color.New(gpuCol).Sprintf("%s, %sused", gpu, gpuUse))
} }
fmt.Printf("\tMemory: System: Physical %s, Swap %s, Reserved %s (%d%% phys)\n",
types.SizeStr(types.NewInt(stat.Info.Resources.MemPhysical)),
types.SizeStr(types.NewInt(stat.Info.Resources.MemSwap)),
types.SizeStr(types.NewInt(stat.Info.Resources.MemReserved)),
stat.Info.Resources.MemReserved*100/stat.Info.Resources.MemPhysical)
fmt.Printf("\t\tUsed: Physical %s (%d%% phys), Virtual %s (%d%% phys, %d%% virt)\n",
types.SizeStr(types.NewInt(stat.MemUsedMin)),
stat.MemUsedMin*100/stat.Info.Resources.MemPhysical,
types.SizeStr(types.NewInt(stat.MemUsedMax)),
stat.MemUsedMax*100/stat.Info.Resources.MemPhysical,
stat.MemUsedMax*100/(stat.Info.Resources.MemPhysical+stat.Info.Resources.MemSwap))
} }
return nil return nil

1
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/davidlazar/go-crypto v0.0.0-20190912175916-7055855a373f // indirect github.com/davidlazar/go-crypto v0.0.0-20190912175916-7055855a373f // indirect
github.com/docker/go-units v0.4.0 github.com/docker/go-units v0.4.0
github.com/drand/drand v0.8.1 github.com/drand/drand v0.8.1
github.com/fatih/color v1.8.0
github.com/filecoin-project/chain-validation v0.0.6-0.20200430201010-ce84f6e96519 github.com/filecoin-project/chain-validation v0.0.6-0.20200430201010-ce84f6e96519
github.com/filecoin-project/filecoin-ffi v0.0.0-20200427223233-a0014b17f124 github.com/filecoin-project/filecoin-ffi v0.0.0-20200427223233-a0014b17f124
github.com/filecoin-project/go-address v0.0.2-0.20200218010043-eb9bb40ed5be github.com/filecoin-project/go-address v0.0.2-0.20200218010043-eb9bb40ed5be