ping.go 4.92 KB
Newer Older
Brian Tiger Chow's avatar
Brian Tiger Chow committed
1 2 3
package commands

import (
Jan Winkelmann's avatar
Jan Winkelmann committed
4
	"context"
5
	"errors"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
6 7
	"fmt"
	"io"
Jeromy's avatar
Jeromy committed
8
	"strings"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
9 10
	"time"

Overbool's avatar
Overbool committed
11
	"github.com/ipfs/go-ipfs/core/commands/cmdenv"
Jan Winkelmann's avatar
Jan Winkelmann committed
12

Jakub Sztandera's avatar
Jakub Sztandera committed
13 14 15 16 17 18 19
	iaddr "github.com/ipfs/go-ipfs-addr"
	cmdkit "github.com/ipfs/go-ipfs-cmdkit"
	cmds "github.com/ipfs/go-ipfs-cmds"
	"github.com/libp2p/go-libp2p-peer"
	pstore "github.com/libp2p/go-libp2p-peerstore"
	ping "github.com/libp2p/go-libp2p/p2p/protocol/ping"
	ma "github.com/multiformats/go-multiaddr"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
20 21
)

Jeromy's avatar
Jeromy committed
22 23
const kPingTimeout = 10 * time.Second

Brian Tiger Chow's avatar
Brian Tiger Chow committed
24 25 26
type PingResult struct {
	Success bool
	Time    time.Duration
27
	Text    string
Brian Tiger Chow's avatar
Brian Tiger Chow committed
28 29
}

Kejie Zhang's avatar
Kejie Zhang committed
30 31 32 33
const (
	pingCountOptionName = "count"
)

34 35 36
// ErrPingSelf is returned when the user attempts to ping themself.
var ErrPingSelf = errors.New("error: can't ping self")

Brian Tiger Chow's avatar
Brian Tiger Chow committed
37
var PingCmd = &cmds.Command{
Jan Winkelmann's avatar
Jan Winkelmann committed
38
	Helptext: cmdkit.HelpText{
Jakub Sztandera's avatar
Jakub Sztandera committed
39
		Tagline: "Send echo request packets to IPFS hosts.",
Brian Tiger Chow's avatar
Brian Tiger Chow committed
40
		ShortDescription: `
41 42
'ipfs ping' is a tool to test sending data to other nodes. It finds nodes
via the routing system, sends pings, waits for pongs, and prints out round-
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
43
trip latency information.
Jeromy's avatar
Jeromy committed
44
		`,
Brian Tiger Chow's avatar
Brian Tiger Chow committed
45
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
46 47
	Arguments: []cmdkit.Argument{
		cmdkit.StringArg("peer ID", true, true, "ID of peer to be pinged.").EnableStdin(),
48
	},
Jan Winkelmann's avatar
Jan Winkelmann committed
49
	Options: []cmdkit.Option{
Kejie Zhang's avatar
Kejie Zhang committed
50
		cmdkit.IntOption(pingCountOptionName, "n", "Number of ping messages to send.").WithDefault(10),
Brian Tiger Chow's avatar
Brian Tiger Chow committed
51
	},
Overbool's avatar
Overbool committed
52 53
	Run: func(req *cmds.Request, res cmds.ResponseEmitter, env cmds.Environment) error {
		n, err := cmdenv.GetNode(env)
Brian Tiger Chow's avatar
Brian Tiger Chow committed
54
		if err != nil {
Overbool's avatar
Overbool committed
55
			return err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
56 57
		}

Jeromy's avatar
Jeromy committed
58
		// Must be online!
59
		if !n.IsOnline {
Overbool's avatar
Overbool committed
60
			return ErrNotOnline
Brian Tiger Chow's avatar
Brian Tiger Chow committed
61 62
		}

Steven Allen's avatar
Steven Allen committed
63
		addr, pid, err := ParsePeerParam(req.Arguments[0])
Jeromy's avatar
Jeromy committed
64
		if err != nil {
Overbool's avatar
Overbool committed
65
			return fmt.Errorf("failed to parse peer address '%s': %s", req.Arguments[0], err)
Jeromy's avatar
Jeromy committed
66 67
		}

Steven Allen's avatar
Steven Allen committed
68
		if pid == n.Identity {
Overbool's avatar
Overbool committed
69
			return ErrPingSelf
70 71
		}

Jeromy's avatar
Jeromy committed
72
		if addr != nil {
Steven Allen's avatar
Steven Allen committed
73
			n.Peerstore.AddAddr(pid, addr, pstore.TempAddrTTL) // temporary
Jeromy's avatar
Jeromy committed
74 75
		}

Overbool's avatar
Overbool committed
76
		numPings, _ := req.Options[pingCountOptionName].(int)
77
		if numPings <= 0 {
Steven Allen's avatar
Steven Allen committed
78
			return fmt.Errorf("ping count must be greater than 0, was %d", numPings)
79 80
		}

81
		if len(n.Peerstore.Addrs(pid)) == 0 {
82
			// Make sure we can find the node in question
Steven Allen's avatar
Steven Allen committed
83
			if err := res.Emit(&PingResult{
84 85
				Text:    fmt.Sprintf("Looking up peer %s", pid.Pretty()),
				Success: true,
Steven Allen's avatar
Steven Allen committed
86 87
			}); err != nil {
				return err
88
			}
Jeromy's avatar
Jeromy committed
89

Steven Allen's avatar
Steven Allen committed
90
			ctx, cancel := context.WithTimeout(req.Context, kPingTimeout)
91
			p, err := n.Routing.FindPeer(ctx, pid)
Steven Allen's avatar
Steven Allen committed
92
			cancel()
93
			if err != nil {
94
				return fmt.Errorf("peer lookup failed: %s", err)
95
			}
Jeromy's avatar
Jeromy committed
96
			n.Peerstore.AddAddrs(p.ID, p.Addrs, pstore.TempAddrTTL)
Jeromy's avatar
Jeromy committed
97
		}
Jeromy's avatar
Jeromy committed
98

Steven Allen's avatar
Steven Allen committed
99
		if err := res.Emit(&PingResult{
100 101
			Text:    fmt.Sprintf("PING %s.", pid.Pretty()),
			Success: true,
Steven Allen's avatar
Steven Allen committed
102 103
		}); err != nil {
			return err
104
		}
Jeromy's avatar
Jeromy committed
105

Steven Allen's avatar
Steven Allen committed
106
		ctx, cancel := context.WithTimeout(req.Context, kPingTimeout*time.Duration(numPings))
Jeromy's avatar
Jeromy committed
107
		defer cancel()
108
		pings := ping.Ping(ctx, n.PeerHost, pid)
Jeromy's avatar
Jeromy committed
109

110 111 112 113
		var (
			count int
			total time.Duration
		)
Steven Allen's avatar
Steven Allen committed
114 115
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
116

Steven Allen's avatar
Steven Allen committed
117
		for i := 0; i < numPings; i++ {
118
			r, ok := <-pings
Steven Allen's avatar
Steven Allen committed
119 120 121 122
			if !ok {
				break
			}

123 124 125 126 127 128 129 130 131 132 133 134 135 136
			if r.Error != nil {
				err = res.Emit(&PingResult{
					Success: false,
					Text:    fmt.Sprintf("Ping error: %s", r.Error),
				})
			} else {
				count++
				total += r.RTT
				err = res.Emit(&PingResult{
					Success: true,
					Time:    r.RTT,
				})
			}
			if err != nil {
Steven Allen's avatar
Steven Allen committed
137 138 139
				return err
			}

140
			select {
Steven Allen's avatar
Steven Allen committed
141
			case <-ticker.C:
142
			case <-ctx.Done():
Steven Allen's avatar
Steven Allen committed
143
				return ctx.Err()
144
			}
Jeromy's avatar
Jeromy committed
145
		}
146
		if count == 0 {
147
			return fmt.Errorf("ping failed")
148 149
		}
		averagems := total.Seconds() * 1000 / float64(count)
Steven Allen's avatar
Steven Allen committed
150
		return res.Emit(&PingResult{
151 152
			Success: true,
			Text:    fmt.Sprintf("Average latency: %.2fms", averagems),
Steven Allen's avatar
Steven Allen committed
153 154 155
		})
	},
	Type: PingResult{},
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
	PostRun: cmds.PostRunMap{
		cmds.CLI: func(res cmds.Response, re cmds.ResponseEmitter) error {
			var (
				total time.Duration
				count int
			)

			for {
				event, err := res.Next()
				switch err {
				case nil:
				case io.EOF:
					return nil
				case context.Canceled, context.DeadlineExceeded:
					if count == 0 {
						return err
					}
					averagems := total.Seconds() * 1000 / float64(count)
					return re.Emit(&PingResult{
						Success: true,
						Text:    fmt.Sprintf("Average latency: %.2fms", averagems),
					})
				default:
					return err
				}

				pr := event.(*PingResult)
				if pr.Success && pr.Text == "" {
					total += pr.Time
					count++
				}
				err = re.Emit(event)
				if err != nil {
					return err
				}
			}
		},
	},
Steven Allen's avatar
Steven Allen committed
194 195 196 197 198 199 200 201 202 203 204 205
	Encoders: cmds.EncoderMap{
		cmds.Text: cmds.MakeTypedEncoder(func(req *cmds.Request, w io.Writer, out *PingResult) error {
			if len(out.Text) > 0 {
				fmt.Fprintln(w, out.Text)
			} else if out.Success {
				fmt.Fprintf(w, "Pong received: time=%.2f ms\n", out.Time.Seconds()*1000)
			} else {
				fmt.Fprintf(w, "Pong failed\n")
			}
			return nil
		}),
	},
Jeromy's avatar
Jeromy committed
206
}
Jeromy's avatar
Jeromy committed
207 208

func ParsePeerParam(text string) (ma.Multiaddr, peer.ID, error) {
Steven Allen's avatar
Steven Allen committed
209 210 211
	// Multiaddr
	if strings.HasPrefix(text, "/") {
		a, err := iaddr.ParseString(text)
Jeromy's avatar
Jeromy committed
212 213 214
		if err != nil {
			return nil, "", err
		}
Steven Allen's avatar
Steven Allen committed
215
		return a.Transport(), a.ID(), nil
Jeromy's avatar
Jeromy committed
216
	}
Steven Allen's avatar
Steven Allen committed
217 218 219
	// Raw peer ID
	p, err := peer.IDB58Decode(text)
	return nil, p, err
Jeromy's avatar
Jeromy committed
220
}