ping.go 4.93 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
	cmds "github.com/ipfs/go-ipfs-cmds"
Raúl Kripalani's avatar
Raúl Kripalani committed
14 15
	peer "github.com/libp2p/go-libp2p-core/peer"
	pstore "github.com/libp2p/go-libp2p-core/peerstore"
Jakub Sztandera's avatar
Jakub Sztandera committed
16 17
	ping "github.com/libp2p/go-libp2p/p2p/protocol/ping"
	ma "github.com/multiformats/go-multiaddr"
Brian Tiger Chow's avatar
Brian Tiger Chow committed
18 19
)

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

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

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

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

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

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

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

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

Overbool's avatar
Overbool committed
74
		numPings, _ := req.Options[pingCountOptionName].(int)
75
		if numPings <= 0 {
Steven Allen's avatar
Steven Allen committed
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
Steven Allen's avatar
Steven Allen committed
81
			if err := res.Emit(&PingResult{
82 83
				Text:    fmt.Sprintf("Looking up peer %s", pid.Pretty()),
				Success: true,
Steven Allen's avatar
Steven Allen committed
84 85
			}); err != nil {
				return err
86
			}
Jeromy's avatar
Jeromy committed
87

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

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

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

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

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

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

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

func ParsePeerParam(text string) (ma.Multiaddr, peer.ID, error) {
Steven Allen's avatar
Steven Allen committed
207 208
	// Multiaddr
	if strings.HasPrefix(text, "/") {
Steven Allen's avatar
Steven Allen committed
209
		maddr, err := ma.NewMultiaddr(text)
Jeromy's avatar
Jeromy committed
210 211 212
		if err != nil {
			return nil, "", err
		}
Steven Allen's avatar
Steven Allen committed
213 214 215 216 217
		transport, id := peer.SplitAddr(maddr)
		if id == "" {
			return nil, "", peer.ErrInvalidAddr
		}
		return transport, id, nil
Jeromy's avatar
Jeromy committed
218
	}
Steven Allen's avatar
Steven Allen committed
219
	// Raw peer ID
220
	p, err := peer.Decode(text)
Steven Allen's avatar
Steven Allen committed
221
	return nil, p, err
Jeromy's avatar
Jeromy committed
222
}