ping.go 4.87 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
	iaddr "github.com/ipfs/go-ipfs-addr"
	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
19 20
)

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

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

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

33 34 35
// 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
36
var PingCmd = &cmds.Command{
Steven Allen's avatar
Steven Allen committed
37
	Helptext: cmds.HelpText{
Jakub Sztandera's avatar
Jakub Sztandera committed
38
		Tagline: "Send echo request packets to IPFS hosts.",
Brian Tiger Chow's avatar
Brian Tiger Chow committed
39
		ShortDescription: `
40 41
'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
42
trip latency information.
Jeromy's avatar
Jeromy committed
43
		`,
Brian Tiger Chow's avatar
Brian Tiger Chow committed
44
	},
Steven Allen's avatar
Steven Allen committed
45 46
	Arguments: []cmds.Argument{
		cmds.StringArg("peer ID", true, true, "ID of peer to be pinged.").EnableStdin(),
47
	},
Steven Allen's avatar
Steven Allen committed
48 49
	Options: []cmds.Option{
		cmds.IntOption(pingCountOptionName, "n", "Number of ping messages to send.").WithDefault(10),
Brian Tiger Chow's avatar
Brian Tiger Chow committed
50
	},
Overbool's avatar
Overbool committed
51 52
	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
53
		if err != nil {
Overbool's avatar
Overbool committed
54
			return err
Brian Tiger Chow's avatar
Brian Tiger Chow committed
55 56
		}

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

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

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

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

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

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

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

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

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

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

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

122 123 124 125 126 127 128 129 130 131 132 133 134 135
			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
136 137 138
				return err
			}

139
			select {
Steven Allen's avatar
Steven Allen committed
140
			case <-ticker.C:
141
			case <-ctx.Done():
Steven Allen's avatar
Steven Allen committed
142
				return ctx.Err()
143
			}
Jeromy's avatar
Jeromy committed
144
		}
145
		if count == 0 {
146
			return fmt.Errorf("ping failed")
147 148
		}
		averagems := total.Seconds() * 1000 / float64(count)
Steven Allen's avatar
Steven Allen committed
149
		return res.Emit(&PingResult{
150 151
			Success: true,
			Text:    fmt.Sprintf("Average latency: %.2fms", averagems),
Steven Allen's avatar
Steven Allen committed
152 153 154
		})
	},
	Type: PingResult{},
155 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
	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
193 194 195 196 197 198 199 200 201 202 203 204
	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
205
}
Jeromy's avatar
Jeromy committed
206 207

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