| 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 | } |