master
go 222 lines 4.9 KB
Raw
1 package commands
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "io"
8 "strings"
9 "time"
10
11 "github.com/ipfs/kubo/core/commands/cmdenv"
12
13 cmds "github.com/ipfs/go-ipfs-cmds"
14 peer "github.com/libp2p/go-libp2p/core/peer"
15 pstore "github.com/libp2p/go-libp2p/core/peerstore"
16 ping "github.com/libp2p/go-libp2p/p2p/protocol/ping"
17 ma "github.com/multiformats/go-multiaddr"
18 )
19
20 const kPingTimeout = 10 * time.Second
21
22 type PingResult struct {
23 Success bool
24 Time time.Duration
25 Text string
26 }
27
28 const (
29 pingCountOptionName = "count"
30 )
31
32 // ErrPingSelf is returned when the user attempts to ping themself.
33 var ErrPingSelf = errors.New("error: can't ping self")
34
35 var PingCmd = &cmds.Command{
36 Helptext: cmds.HelpText{
37 Tagline: "Send echo request packets to IPFS hosts.",
38 ShortDescription: `
39 'ipfs ping' is a tool to test sending data to other nodes. It finds nodes
40 via the routing system, sends pings, waits for pongs, and prints out round-
41 trip latency information.
42 `,
43 },
44 Arguments: []cmds.Argument{
45 cmds.StringArg("peer ID", true, true, "ID of peer to be pinged.").EnableStdin(),
46 },
47 Options: []cmds.Option{
48 cmds.IntOption(pingCountOptionName, "n", "Number of ping messages to send.").WithDefault(10),
49 },
50 Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
51 n, err := cmdenv.GetNode(env)
52 if err != nil {
53 return err
54 }
55
56 // Must be online!
57 if !n.IsOnline {
58 return ErrNotOnline
59 }
60
61 addr, pid, err := ParsePeerParam(req.Arguments[0])
62 if err != nil {
63 return fmt.Errorf("failed to parse peer address '%s': %s", req.Arguments[0], err)
64 }
65
66 if pid == n.Identity {
67 return ErrPingSelf
68 }
69
70 if addr != nil {
71 n.Peerstore.AddAddr(pid, addr, pstore.TempAddrTTL) // temporary
72 }
73
74 numPings, _ := req.Options[pingCountOptionName].(int)
75 if numPings <= 0 {
76 return fmt.Errorf("ping count must be greater than 0, was %d", numPings)
77 }
78
79 if len(n.Peerstore.Addrs(pid)) == 0 {
80 // Make sure we can find the node in question
81 if err := res.Emit(&PingResult{
82 Text: fmt.Sprintf("Looking up peer %s", pid),
83 Success: true,
84 }); err != nil {
85 return err
86 }
87
88 ctx, cancel := context.WithTimeout(req.Context, kPingTimeout)
89 p, err := n.Routing.FindPeer(ctx, pid)
90 cancel()
91 if err != nil {
92 return fmt.Errorf("peer lookup failed: %s", err)
93 }
94 n.Peerstore.AddAddrs(p.ID, p.Addrs, pstore.TempAddrTTL)
95 }
96
97 if err := res.Emit(&PingResult{
98 Text: fmt.Sprintf("PING %s.", pid),
99 Success: true,
100 }); err != nil {
101 return err
102 }
103
104 ctx, cancel := context.WithTimeout(req.Context, kPingTimeout*time.Duration(numPings))
105 defer cancel()
106 pings := ping.Ping(ctx, n.PeerHost, pid)
107
108 var (
109 count int
110 total time.Duration
111 )
112 ticker := time.NewTicker(time.Second)
113 defer ticker.Stop()
114
115 for range numPings {
116 r, ok := <-pings
117 if !ok {
118 break
119 }
120
121 if r.Error != nil {
122 err = res.Emit(&PingResult{
123 Success: false,
124 Text: fmt.Sprintf("Ping error: %s", r.Error),
125 })
126 } else {
127 count++
128 total += r.RTT
129 err = res.Emit(&PingResult{
130 Success: true,
131 Time: r.RTT,
132 })
133 }
134 if err != nil {
135 return err
136 }
137
138 select {
139 case <-ticker.C:
140 case <-ctx.Done():
141 return ctx.Err()
142 }
143 }
144 if count == 0 {
145 return fmt.Errorf("ping failed")
146 }
147 averagems := total.Seconds() * 1000 / float64(count)
148 return res.Emit(&PingResult{
149 Success: true,
150 Text: fmt.Sprintf("Average latency: %.2fms", averagems),
151 })
152 },
153 Type: PingResult{},
154 PostRun: cmds.PostRunMap{
155 cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
156 var (
157 total time.Duration
158 count int
159 )
160
161 for {
162 event, err := res.Next()
163 switch err {
164 case nil:
165 case io.EOF:
166 return nil
167 case context.Canceled, context.DeadlineExceeded:
168 if count == 0 {
169 return err
170 }
171 averagems := total.Seconds() * 1000 / float64(count)
172 return re.Emit(&PingResult{
173 Success: true,
174 Text: fmt.Sprintf("Average latency: %.2fms", averagems),
175 })
176 default:
177 return err
178 }
179
180 pr := event.(*PingResult)
181 if pr.Success && pr.Text == "" {
182 total += pr.Time
183 count++
184 }
185 err = re.Emit(event)
186 if err != nil {
187 return err
188 }
189 }
190 },
191 },
192 Encoders: cmds.EncoderMap{
193 cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *PingResult) error {
194 if len(out.Text) > 0 {
195 fmt.Fprintln(w, out.Text)
196 } else if out.Success {
197 fmt.Fprintf(w, "Pong received: time=%.2f ms\n", out.Time.Seconds()*1000)
198 } else {
199 fmt.Fprintf(w, "Pong failed\n")
200 }
201 return nil
202 }),
203 },
204 }
205
206 func ParsePeerParam(text string) (ma.Multiaddr, peer.ID, error) {
207 // Multiaddr
208 if strings.HasPrefix(text, "/") {
209 maddr, err := ma.NewMultiaddr(text)
210 if err != nil {
211 return nil, "", err
212 }
213 transport, id := peer.SplitAddr(maddr)
214 if id == "" {
215 return nil, "", peer.ErrInvalidAddr
216 }
217 return transport, id, nil
218 }
219 // Raw peer ID
220 p, err := peer.Decode(text)
221 return nil, p, err
222 }