Skip to content
This repository was archived by the owner on Feb 12, 2024. It is now read-only.

Commit a181932

Browse files
JGAntunesalanshaw
authored andcommitted
feat: ping (#1299)
* feat: implement `ipfs ping` flags #928 * feat: first shot at ping implementaion * fix: ETOOMANYSTREAMS 😢 * chore: cleanup on the ping component * chore: ping component linting * chore: bump js-ipfs-api and fix http ping validation * chore: add test to ping cli command * chore: add ping cli test * chore: refactor ping component and some cleanup * chore: add tests to the ping http API * fix: no need to check for peerRouting method in ping * chore: add tests for ping core functionality
1 parent 95e74cc commit a181932

File tree

11 files changed

+610
-3
lines changed

11 files changed

+610
-3
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"progress": "^2.0.0",
155155
"promisify-es6": "^1.0.3",
156156
"pull-abortable": "^4.1.1",
157+
"pull-catch": "^1.0.0",
157158
"pull-defer": "^0.2.2",
158159
"pull-file": "^1.1.0",
159160
"pull-ndjson": "^0.1.1",

src/cli/commands/ping.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
'use strict'
2+
3+
const pull = require('pull-stream/pull')
4+
const drain = require('pull-stream/sinks/drain')
5+
const pullCatch = require('pull-catch')
6+
7+
const print = require('../utils').print
8+
9+
module.exports = {
10+
command: 'ping <peerId>',
11+
12+
description: 'Measure the latency of a connection',
13+
14+
builder: {
15+
count: {
16+
alias: 'n',
17+
type: 'integer',
18+
default: 10
19+
}
20+
},
21+
22+
handler (argv) {
23+
const peerId = argv.peerId
24+
const count = argv.count || 10
25+
pull(
26+
argv.ipfs.pingPullStream(peerId, { count }),
27+
pullCatch(err => {
28+
throw err
29+
}),
30+
drain(({ Time, Text }) => {
31+
// Check if it's a pong
32+
if (Time) {
33+
print(`Pong received: time=${Time} ms`)
34+
// Status response
35+
} else {
36+
print(Text)
37+
}
38+
})
39+
)
40+
}
41+
}

src/core/components/ping.js

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,96 @@
11
'use strict'
22

33
const promisify = require('promisify-es6')
4+
const debug = require('debug')
5+
const OFFLINE_ERROR = require('../utils').OFFLINE_ERROR
6+
const PeerId = require('peer-id')
7+
const pull = require('pull-stream/pull')
8+
const Pushable = require('pull-pushable')
9+
const ndjson = require('pull-ndjson')
10+
const waterfall = require('async/waterfall')
11+
12+
const log = debug('jsipfs:ping')
13+
log.error = debug('jsipfs:ping:error')
414

515
module.exports = function ping (self) {
6-
return promisify((callback) => {
7-
callback(new Error('Not implemented'))
16+
return promisify((peerId, count, cb) => {
17+
if (!self.isOnline()) {
18+
return cb(new Error(OFFLINE_ERROR))
19+
}
20+
21+
const source = Pushable()
22+
23+
const response = pull(
24+
source,
25+
ndjson.serialize()
26+
)
27+
waterfall([
28+
getPeer.bind(null, self._libp2pNode, source, peerId),
29+
runPing.bind(null, self._libp2pNode, source, count)
30+
], (err) => {
31+
if (err) {
32+
log.error(err)
33+
source.push(getPacket({Text: err.toString()}))
34+
source.end(err)
35+
}
36+
})
37+
38+
cb(null, response)
39+
})
40+
}
41+
42+
function getPacket (msg) {
43+
// Default msg
44+
const basePacket = {Success: false, Time: 0, Text: ''}
45+
return Object.assign({}, basePacket, msg)
46+
}
47+
48+
function getPeer (libp2pNode, statusStream, peerId, cb) {
49+
let peer
50+
try {
51+
peer = libp2pNode.peerBook.get(peerId)
52+
return cb(null, peer)
53+
} catch (err) {
54+
log('Peer not found in peer book, trying peer routing')
55+
// Share lookup status just as in the go implemmentation
56+
statusStream.push(getPacket({Success: true, Text: `Looking up peer ${peerId}`}))
57+
// Try to use peerRouting
58+
libp2pNode.peerRouting.findPeer(PeerId.createFromB58String(peerId), cb)
59+
}
60+
}
61+
62+
function runPing (libp2pNode, statusStream, count, peer, cb) {
63+
libp2pNode.ping(peer, (err, p) => {
64+
log('Got peer', peer)
65+
if (err) {
66+
return cb(err)
67+
}
68+
69+
let packetCount = 0
70+
let totalTime = 0
71+
statusStream.push(getPacket({Success: true, Text: `PING ${peer.id.toB58String()}`}))
72+
73+
p.on('ping', (time) => {
74+
statusStream.push(getPacket({ Success: true, Time: time }))
75+
totalTime += time
76+
packetCount++
77+
if (packetCount >= count) {
78+
const average = totalTime / count
79+
p.stop()
80+
statusStream.push(getPacket({ Success: true, Text: `Average latency: ${average}ms` }))
81+
statusStream.end()
82+
}
83+
})
84+
85+
p.on('error', (err) => {
86+
log.error(err)
87+
p.stop()
88+
statusStream.push(getPacket({Text: err.toString()}))
89+
statusStream.end(err)
90+
})
91+
92+
p.start()
93+
94+
return cb()
895
})
996
}

src/http/api/resources/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
exports.version = require('./version')
44
exports.shutdown = require('./shutdown')
55
exports.id = require('./id')
6+
exports.ping = require('./ping')
67
exports.bootstrap = require('./bootstrap')
78
exports.repo = require('./repo')
89
exports.object = require('./object')

src/http/api/resources/ping.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
'use strict'
2+
3+
const Joi = require('joi')
4+
const boom = require('boom')
5+
const toStream = require('pull-stream-to-stream')
6+
const PassThrough = require('readable-stream').PassThrough
7+
const pump = require('pump')
8+
9+
exports = module.exports
10+
11+
exports.get = {
12+
validate: {
13+
query: Joi.object().keys({
14+
n: Joi.number().greater(0),
15+
count: Joi.number().greater(0),
16+
arg: Joi.string().required()
17+
}).xor('n', 'count').unknown()
18+
},
19+
handler: (request, reply) => {
20+
const ipfs = request.server.app.ipfs
21+
const peerId = request.query.arg
22+
// Default count to 10
23+
const count = request.query.n || request.query.count || 10
24+
ipfs.ping(peerId, count, (err, pullStream) => {
25+
if (err) {
26+
return reply(boom.badRequest(err))
27+
}
28+
// Streams from pull-stream-to-stream don't seem to be compatible
29+
// with the stream2 readable interface
30+
// see: https://github.com/hapijs/hapi/blob/c23070a3de1b328876d5e64e679a147fafb04b38/lib/response.js#L533
31+
// and: https://github.com/pull-stream/pull-stream-to-stream/blob/e436acee18b71af8e71d1b5d32eee642351517c7/index.js#L28
32+
const responseStream = toStream.source(pullStream)
33+
const stream2 = new PassThrough()
34+
pump(responseStream, stream2)
35+
return reply(stream2).type('application/json').header('X-Chunked-Output', '1')
36+
})
37+
}
38+
}

src/http/api/routes/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module.exports = (server) => {
99
require('./object')(server)
1010
require('./repo')(server)
1111
require('./config')(server)
12+
require('./ping')(server)
1213
require('./swarm')(server)
1314
require('./bitswap')(server)
1415
require('./file')(server)

src/http/api/routes/ping.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict'
2+
3+
const resources = require('./../resources')
4+
5+
module.exports = (server) => {
6+
const api = server.select('API')
7+
8+
api.route({
9+
method: '*',
10+
path: '/api/v0/ping',
11+
config: {
12+
handler: resources.ping.get.handler,
13+
validate: resources.ping.get.validate
14+
}
15+
})
16+
}

test/cli/commands.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
const expect = require('chai').expect
55
const runOnAndOff = require('../utils/on-and-off')
66

7-
const commandCount = 73
7+
const commandCount = 74
8+
89
describe('commands', () => runOnAndOff((thing) => {
910
let ipfs
1011

test/cli/ping.js

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* eslint max-nested-callbacks: ["error", 8] */
2+
/* eslint-env mocha */
3+
'use strict'
4+
5+
const chai = require('chai')
6+
const dirtyChai = require('dirty-chai')
7+
const series = require('async/series')
8+
const DaemonFactory = require('ipfsd-ctl')
9+
const ipfsExec = require('../utils/ipfs-exec')
10+
11+
const df = DaemonFactory.create({ type: 'js' })
12+
const expect = chai.expect
13+
chai.use(dirtyChai)
14+
15+
const config = {
16+
Bootstrap: [],
17+
Discovery: {
18+
MDNS: {
19+
Enabled:
20+
false
21+
}
22+
}
23+
}
24+
25+
describe('ping', function () {
26+
this.timeout(60 * 1000)
27+
let ipfsdA
28+
let ipfsdB
29+
let bMultiaddr
30+
let ipfsdBId
31+
let cli
32+
33+
before((done) => {
34+
this.timeout(60 * 1000)
35+
series([
36+
(cb) => {
37+
df.spawn({
38+
exec: `./src/cli/bin.js`,
39+
config,
40+
initOptions: { bits: 512 }
41+
}, (err, _ipfsd) => {
42+
expect(err).to.not.exist()
43+
ipfsdB = _ipfsd
44+
cb()
45+
})
46+
},
47+
(cb) => {
48+
ipfsdB.api.id((err, peerInfo) => {
49+
expect(err).to.not.exist()
50+
ipfsdBId = peerInfo.id
51+
bMultiaddr = peerInfo.addresses[0]
52+
cb()
53+
})
54+
}
55+
], done)
56+
})
57+
58+
before(function (done) {
59+
this.timeout(60 * 1000)
60+
61+
df.spawn({
62+
exec: './src/cli/bin.js',
63+
config,
64+
initoptions: { bits: 512 }
65+
}, (err, _ipfsd) => {
66+
expect(err).to.not.exist()
67+
ipfsdA = _ipfsd
68+
// Without DHT we need to have an already established connection
69+
ipfsdA.api.swarm.connect(bMultiaddr, done)
70+
})
71+
})
72+
73+
before((done) => {
74+
this.timeout(60 * 1000)
75+
cli = ipfsExec(ipfsdA.repoPath)
76+
done()
77+
})
78+
79+
after((done) => ipfsdA.stop(done))
80+
after((done) => ipfsdB.stop(done))
81+
82+
it('ping host', (done) => {
83+
this.timeout(60 * 1000)
84+
const ping = cli(`ping ${ipfsdBId}`)
85+
const result = []
86+
ping.stdout.on('data', (output) => {
87+
const packets = output.toString().split('\n').slice(0, -1)
88+
result.push(...packets)
89+
})
90+
91+
ping.stdout.on('end', () => {
92+
expect(result).to.have.lengthOf(12)
93+
expect(result[0]).to.equal(`PING ${ipfsdBId}`)
94+
for (let i = 1; i < 11; i++) {
95+
expect(result[i]).to.match(/^Pong received: time=\d+ ms$/)
96+
}
97+
expect(result[11]).to.match(/^Average latency: \d+(.\d+)?ms$/)
98+
done()
99+
})
100+
101+
ping.catch((err) => {
102+
expect(err).to.not.exist()
103+
})
104+
})
105+
106+
it('ping host with --n option', (done) => {
107+
this.timeout(60 * 1000)
108+
const ping = cli(`ping --n 1 ${ipfsdBId}`)
109+
const result = []
110+
ping.stdout.on('data', (output) => {
111+
const packets = output.toString().split('\n').slice(0, -1)
112+
result.push(...packets)
113+
})
114+
115+
ping.stdout.on('end', () => {
116+
expect(result).to.have.lengthOf(3)
117+
expect(result[0]).to.equal(`PING ${ipfsdBId}`)
118+
expect(result[1]).to.match(/^Pong received: time=\d+ ms$/)
119+
expect(result[2]).to.match(/^Average latency: \d+(.\d+)?ms$/)
120+
done()
121+
})
122+
123+
ping.catch((err) => {
124+
expect(err).to.not.exist()
125+
})
126+
})
127+
128+
it('ping host with --count option', (done) => {
129+
this.timeout(60 * 1000)
130+
const ping = cli(`ping --count 1 ${ipfsdBId}`)
131+
const result = []
132+
ping.stdout.on('data', (output) => {
133+
const packets = output.toString().split('\n').slice(0, -1)
134+
result.push(...packets)
135+
})
136+
137+
ping.stdout.on('end', () => {
138+
expect(result).to.have.lengthOf(3)
139+
expect(result[0]).to.equal(`PING ${ipfsdBId}`)
140+
expect(result[1]).to.match(/^Pong received: time=\d+ ms$/)
141+
expect(result[2]).to.match(/^Average latency: \d+(.\d+)?ms$/)
142+
done()
143+
})
144+
145+
ping.catch((err) => {
146+
expect(err).to.not.exist()
147+
})
148+
})
149+
})

0 commit comments

Comments
 (0)